mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Feat/combine uis (#2633)
* wip * restructure backend for new ui structure * new patchdb bootstrap, single websocket api, local storage migration, more * update db websocket * init apis * update patch-db * setup progress * feat: implement state service, alert and routing Signed-off-by: waterplea <alexander@inkin.ru> * update setup wizard for new types * feat: add init page Signed-off-by: waterplea <alexander@inkin.ru> * chore: refactor message, patch-db source stream and connection service Signed-off-by: waterplea <alexander@inkin.ru> * fix method not found on state * fix backend bugs * fix compat assets * address comments * remove unneeded styling * cleaner progress * bugfixes * fix init logs * fix progress reporting * fix navigation by getting state after init * remove patch dependency from live api * fix caching * re-add patchDB to live api * fix metrics values * send close frame * add bootId and fix polling --------- Signed-off-by: waterplea <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
19
web/package-lock.json
generated
19
web/package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"@taiga-ui/core": "3.20.0",
|
||||
"@taiga-ui/icons": "3.20.0",
|
||||
"@taiga-ui/kit": "3.20.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"angular-svg-round-progressbar": "^9.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -1973,7 +1974,7 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-rev0.lib0.rc8.beta10",
|
||||
"version": "0.3.6-alpha1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@@ -5432,6 +5433,20 @@
|
||||
"rxjs": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tinkoff/ng-dompurify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tinkoff/ng-dompurify/-/ng-dompurify-4.0.0.tgz",
|
||||
"integrity": "sha512-BjKUweWLrOx8UOZw+Tl+Dae5keYuSbeMkppcXQdsvwASMrPfmP7d3Q206Q6HDqOV2WnpnFqGUB95IMbLAeRRuw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=12.0.0",
|
||||
"@angular/platform-browser": ">=12.0.0",
|
||||
"@types/dompurify": ">=2.3.0",
|
||||
"dompurify": ">= 2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tinkoff/ng-event-plugins": {
|
||||
"version": "3.1.0",
|
||||
"license": "Apache-2.0",
|
||||
@@ -5549,7 +5564,6 @@
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "2.3.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
@@ -5726,7 +5740,6 @@
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)",
|
||||
"build:dui": "ng run diagnostic-ui:build",
|
||||
"build:install-wiz": "ng run install-wizard:build",
|
||||
"build:setup": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build",
|
||||
@@ -25,7 +24,6 @@
|
||||
"analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json",
|
||||
"publish:shared": "npm run build:shared && npm publish ./dist/shared --access public",
|
||||
"publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public",
|
||||
"start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0",
|
||||
"start:install-wiz": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0",
|
||||
"start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0",
|
||||
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0",
|
||||
@@ -56,6 +54,7 @@
|
||||
"@taiga-ui/core": "3.20.0",
|
||||
"@taiga-ui/icons": "3.20.0",
|
||||
"@taiga-ui/kit": "3.20.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"angular-svg-round-progressbar": "^9.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadChildren: () =>
|
||||
import('./pages/logs/logs.module').then(m => m.LogsPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
scrollPositionRestoration: 'enabled',
|
||||
preloadingStrategy: PreloadAllModules,
|
||||
useHash: true,
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<tui-root>
|
||||
<ion-app>
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
@@ -1,8 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tui-root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||
import { TuiRootModule } from '@taiga-ui/core'
|
||||
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 { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
ui: { api },
|
||||
} = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot({
|
||||
mode: 'md',
|
||||
}),
|
||||
AppRoutingModule,
|
||||
TuiRootModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { HomePage } from './home.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class HomePageRoutingModule {}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
|
||||
export abstract class ApiService {
|
||||
abstract getError(): Promise<GetErrorRes>
|
||||
abstract restart(): Promise<void>
|
||||
abstract forgetDrive(): Promise<void>
|
||||
abstract repairDisk(): Promise<void>
|
||||
abstract systemRebuild(): Promise<void>
|
||||
abstract getLogs(params: ServerLogsReq): Promise<LogsRes>
|
||||
}
|
||||
|
||||
export interface GetErrorRes {
|
||||
code: number
|
||||
message: string
|
||||
data: { details: string }
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
async getError(): Promise<GetErrorRes> {
|
||||
return this.rpcRequest<GetErrorRes>({
|
||||
method: 'diagnostic.error',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.forget',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async repairDisk(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.repair',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async systemRebuild(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.rebuild',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
||||
return this.rpcRequest<LogsRes>({
|
||||
method: 'diagnostic.logs',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { ApiService, GetErrorRes } from './api.service'
|
||||
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService implements ApiService {
|
||||
async getError(): Promise<GetErrorRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async repairDisk(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async systemRebuild(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async getLogs(params: ServerLogsReq): Promise<LogsRes> {
|
||||
await pauseFor(1000)
|
||||
let entries: Log[]
|
||||
if (Math.random() < 0.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,
|
||||
startCursor: 'start-cursor',
|
||||
endCursor: 'end-cursor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 *****',
|
||||
},
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
}
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>StartOS Diagnostic UI</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/icon/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { AppModule } from './app/app.module'
|
||||
import { environment } from './environments/environment'
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err))
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
@@ -1,41 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf');
|
||||
}
|
||||
|
||||
/** Ionic CSS Variables overrides **/
|
||||
:root {
|
||||
--ion-font-family: 'Montserrat';
|
||||
|
||||
--ion-color-primary: #0075e1;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
|
||||
--ion-item-background: #2b2b2b;
|
||||
--ion-toolbar-background: #2b2b2b;
|
||||
--ion-card-background: #2b2b2b;
|
||||
|
||||
--ion-background-color: #282828;
|
||||
--ion-background-color-rgb: 30,30,30;
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
--ion-text-color-rgb: var(--ion-color-dark-rgb);
|
||||
}
|
||||
|
||||
.loader {
|
||||
--spinner-color: var(--ion-color-warning) !important;
|
||||
z-index: 40000 !important;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Prevents Angular change detection from
|
||||
* running with certain Web Component callbacks
|
||||
*/
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(window as any).__Zone_disable_customElements = true
|
||||
@@ -1,9 +0,0 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export class AppComponent {
|
||||
|
||||
let route = '/home'
|
||||
if (inProgress) {
|
||||
route = inProgress.complete ? '/success' : '/loading'
|
||||
route = inProgress.status === 'complete' ? '/success' : '/loading'
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(route)
|
||||
|
||||
@@ -5,12 +5,7 @@ import {
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
ApiService,
|
||||
BackupRecoverySource,
|
||||
DiskRecoverySource,
|
||||
DiskMigrateSource,
|
||||
} from 'src/app/services/api/api.service'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
@@ -58,18 +53,17 @@ export class EmbassyPage {
|
||||
} else if (this.stateService.setupType === 'restore') {
|
||||
this.storageDrives = disks.filter(
|
||||
d =>
|
||||
this.stateService.recoverySource?.type === 'backup' &&
|
||||
this.stateService.recoverySource.target?.type === 'disk' &&
|
||||
!d.partitions
|
||||
.map(p => p.logicalname)
|
||||
.includes(
|
||||
(
|
||||
(this.stateService.recoverySource as BackupRecoverySource)
|
||||
?.target as DiskRecoverySource
|
||||
)?.logicalname,
|
||||
),
|
||||
.includes(this.stateService.recoverySource.target.logicalname),
|
||||
)
|
||||
} else if (this.stateService.setupType === 'transfer') {
|
||||
const guid = (this.stateService.recoverySource as DiskMigrateSource)
|
||||
.guid
|
||||
} else if (
|
||||
this.stateService.setupType === 'transfer' &&
|
||||
this.stateService.recoverySource?.type === 'migrate'
|
||||
) {
|
||||
const guid = this.stateService.recoverySource.guid
|
||||
this.storageDrives = disks.filter(d => {
|
||||
return (
|
||||
d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid)
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { LoadingPage, ToMessagePipe } from './loading.page'
|
||||
import { LoadingPage } from './loading.page'
|
||||
import { LoadingPageRoutingModule } from './loading-routing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule],
|
||||
declarations: [LoadingPage, ToMessagePipe],
|
||||
declarations: [LoadingPage],
|
||||
})
|
||||
export class LoadingPageModule {}
|
||||
|
||||
@@ -1,39 +1,17 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-card *ngIf="progress$ | async as progress" color="dark">
|
||||
<ion-card-header>
|
||||
<ion-card-title>Initializing StartOS</ion-card-title>
|
||||
<div class="center-wrapper">
|
||||
<ion-card-subtitle>
|
||||
{{ progress.transferred | toMessage }}
|
||||
</ion-card-subtitle>
|
||||
</div>
|
||||
</ion-card-header>
|
||||
<section *ngIf="progress$ | async as progress">
|
||||
<h1 [style.font-size.rem]="2.5" [style.margin.rem]="1">
|
||||
Setting up your server
|
||||
</h1>
|
||||
<div class="center-wrapper" *ngIf="progress.total">
|
||||
Progress: {{ (progress.total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<ion-progress-bar
|
||||
color="tertiary"
|
||||
style="max-width: 700px; margin: auto; margin-bottom: 36px"
|
||||
[type]="progress.transferred && progress.transferred < 1 ? 'determinate' : 'indeterminate'"
|
||||
[value]="progress.transferred || 0"
|
||||
></ion-progress-bar>
|
||||
<p>
|
||||
<ng-container *ngIf="progress.totalBytes as total">
|
||||
<ng-container
|
||||
*ngIf="progress.transferred as transferred; else calculating"
|
||||
>
|
||||
Progress: {{ (transferred * 100).toFixed() }}%
|
||||
</ng-container>
|
||||
<ng-template #calculating>
|
||||
{{ (progress.totalBytes / 1073741824).toFixed(2) }} GB
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
class="progress"
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress.total"
|
||||
></progress>
|
||||
<p>{{ progress.message }}</p>
|
||||
</section>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ion-card-title {
|
||||
font-size: 42px;
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import {
|
||||
EMPTY,
|
||||
Observable,
|
||||
catchError,
|
||||
filter,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ErrorToastService, pauseFor } from '@start9labs/shared'
|
||||
|
||||
type Progress = {
|
||||
totalBytes: number | null
|
||||
transferred: number
|
||||
}
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading',
|
||||
@@ -17,10 +25,46 @@ type Progress = {
|
||||
styleUrls: ['loading.page.scss'],
|
||||
})
|
||||
export class LoadingPage {
|
||||
readonly progress$ = new BehaviorSubject<Progress>({
|
||||
totalBytes: null,
|
||||
transferred: 0,
|
||||
})
|
||||
readonly progress$ = this.getRunningStatus$().pipe(
|
||||
switchMap(res =>
|
||||
this.api.openProgressWebsocket$(res.guid).pipe(
|
||||
startWith(res.progress),
|
||||
catchError((_, watch$) => {
|
||||
return interval(2000).pipe(
|
||||
switchMap(() =>
|
||||
from(this.api.getStatus()).pipe(catchError(() => EMPTY)),
|
||||
),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
)
|
||||
}),
|
||||
tap(progress => {
|
||||
if (progress.overall === true) {
|
||||
this.getStatus()
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
map(({ phases, overall }) => {
|
||||
return {
|
||||
total: getDecimal(overall),
|
||||
message: phases
|
||||
.filter(
|
||||
(
|
||||
p,
|
||||
): p is {
|
||||
name: string
|
||||
progress: {
|
||||
done: number
|
||||
total: number | null
|
||||
}
|
||||
} => p.progress !== true && p.progress !== null,
|
||||
)
|
||||
.map(p => `${p.name}${getPhaseBytes(p.progress)}`)
|
||||
.join(','),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
@@ -28,55 +72,55 @@ export class LoadingPage {
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.poll()
|
||||
}
|
||||
private async getStatus(): Promise<{
|
||||
status: 'running'
|
||||
guid: string
|
||||
progress: T.FullProgress
|
||||
} | void> {
|
||||
const res = await this.api.getStatus()
|
||||
|
||||
async poll() {
|
||||
try {
|
||||
const progress = await this.api.getStatus()
|
||||
|
||||
if (!progress) return
|
||||
|
||||
const { totalBytes, bytesTransferred } = progress
|
||||
|
||||
this.progress$.next({
|
||||
totalBytes,
|
||||
transferred: totalBytes ? bytesTransferred / totalBytes : 0,
|
||||
})
|
||||
|
||||
if (progress.complete) {
|
||||
this.navCtrl.navigateForward(`/success`)
|
||||
this.progress$.complete()
|
||||
return
|
||||
}
|
||||
|
||||
await pauseFor(250)
|
||||
|
||||
setTimeout(() => this.poll(), 0) // prevent call stack from growing
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toMessage',
|
||||
})
|
||||
export class ToMessagePipe implements PipeTransform {
|
||||
constructor(private readonly stateService: StateService) {}
|
||||
|
||||
transform(progress: number | null): string {
|
||||
if (['fresh', 'attach'].includes(this.stateService.setupType || '')) {
|
||||
return 'Setting up your server'
|
||||
}
|
||||
|
||||
if (!progress) {
|
||||
return 'Calculating size'
|
||||
} else if (progress < 1) {
|
||||
return 'Copying data'
|
||||
if (!res) {
|
||||
this.navCtrl.navigateRoot('/home')
|
||||
} else if (res.status === 'complete') {
|
||||
this.navCtrl.navigateForward(`/success`)
|
||||
} else {
|
||||
return 'Finalizing'
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
private getRunningStatus$(): Observable<{
|
||||
status: 'running'
|
||||
guid: string
|
||||
progress: T.FullProgress
|
||||
}> {
|
||||
return from(this.getStatus()).pipe(
|
||||
filter(Boolean),
|
||||
catchError(e => {
|
||||
this.errorToastService.present(e)
|
||||
return of(e)
|
||||
}),
|
||||
take(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getDecimal(progress: T.Progress): number {
|
||||
if (progress === true) {
|
||||
return 1
|
||||
} else if (!progress || !progress.total) {
|
||||
return 0
|
||||
} else {
|
||||
return progress.total && progress.done / progress.total
|
||||
}
|
||||
}
|
||||
|
||||
function getPhaseBytes(
|
||||
progress:
|
||||
| false
|
||||
| {
|
||||
done: number
|
||||
total: number | null
|
||||
},
|
||||
): string {
|
||||
return progress === false ? '' : `: (${progress.done}/${progress.total})`
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import * as jose from 'node-jose'
|
||||
import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export abstract class ApiService {
|
||||
pubkey?: jose.JWK.Key
|
||||
|
||||
abstract getStatus(): Promise<StatusRes> // setup.status
|
||||
abstract getStatus(): Promise<T.SetupStatusRes | null> // setup.status
|
||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<StartOSDiskInfo> // setup.cifs.verify
|
||||
abstract attach(importInfo: AttachReq): Promise<void> // setup.attach
|
||||
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
||||
abstract complete(): Promise<CompleteRes> // setup.complete
|
||||
abstract verifyCifs(cifs: T.VerifyCifsParams): Promise<StartOSDiskInfo> // setup.cifs.verify
|
||||
abstract attach(importInfo: T.AttachParams): Promise<T.SetupProgress> // setup.attach
|
||||
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract openProgressWebsocket$(guid: string): Observable<T.FullProgress>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||
if (!this.pubkey) throw new Error('No pubkey found!')
|
||||
@@ -27,29 +32,7 @@ type Encrypted = {
|
||||
encrypted: string
|
||||
}
|
||||
|
||||
export type StatusRes = {
|
||||
bytesTransferred: number
|
||||
totalBytes: number | null
|
||||
complete: boolean
|
||||
} | null
|
||||
|
||||
export type AttachReq = {
|
||||
guid: string
|
||||
startOsPassword: Encrypted
|
||||
}
|
||||
|
||||
export type ExecuteReq = {
|
||||
startOsLogicalname: string
|
||||
startOsPassword: Encrypted
|
||||
recoverySource: RecoverySource | null
|
||||
recoveryPassword: Encrypted | null
|
||||
}
|
||||
|
||||
export type CompleteRes = {
|
||||
torAddress: string
|
||||
lanAddress: string
|
||||
rootCa: string
|
||||
}
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
export type DiskBackupTarget = {
|
||||
vendor: string | null
|
||||
@@ -68,27 +51,3 @@ export type CifsBackupTarget = {
|
||||
mountable: boolean
|
||||
startOs: StartOSDiskInfo | null
|
||||
}
|
||||
|
||||
export type DiskRecoverySource = {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export type BackupRecoverySource = {
|
||||
type: 'backup'
|
||||
target: CifsRecoverySource | DiskRecoverySource
|
||||
}
|
||||
export type RecoverySource = BackupRecoverySource | DiskMigrateSource
|
||||
|
||||
export type DiskMigrateSource = {
|
||||
type: 'migrate'
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type CifsRecoverySource = {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: Encrypted | null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
@@ -8,27 +8,35 @@ import {
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
DiskRecoverySource,
|
||||
StatusRes,
|
||||
AttachReq,
|
||||
ExecuteReq,
|
||||
CompleteRes,
|
||||
} from './api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ApiService, WebsocketConfig } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
import { Observable } from 'rxjs'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor(private readonly http: HttpService) {
|
||||
constructor(
|
||||
private readonly http: HttpService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
return this.rpcRequest<StatusRes>({
|
||||
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
|
||||
const { location } = this.document.defaultView!
|
||||
const host = location.host
|
||||
|
||||
return webSocket({
|
||||
url: `ws://${host}/ws/rpc/${guid}`,
|
||||
})
|
||||
}
|
||||
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
return this.rpcRequest<T.SetupStatusRes | null>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
@@ -41,7 +49,7 @@ export class LiveApiService extends ApiService {
|
||||
* this wil all public/private key, which means that there is no information loss
|
||||
* through the network.
|
||||
*/
|
||||
async getPubKey() {
|
||||
async getPubKey(): Promise<void> {
|
||||
const response: jose.JWK.Key = await this.rpcRequest({
|
||||
method: 'setup.get-pubkey',
|
||||
params: {},
|
||||
@@ -50,14 +58,14 @@ export class LiveApiService extends ApiService {
|
||||
this.pubkey = response
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
return this.rpcRequest<DiskListResponse>({
|
||||
method: 'setup.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async verifyCifs(source: CifsRecoverySource) {
|
||||
async verifyCifs(source: T.VerifyCifsParams): Promise<StartOSDiskInfo> {
|
||||
source.path = source.path.replace('/\\/g', '/')
|
||||
return this.rpcRequest<StartOSDiskInfo>({
|
||||
method: 'setup.cifs.verify',
|
||||
@@ -65,14 +73,14 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async attach(params: AttachReq) {
|
||||
await this.rpcRequest<void>({
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.attach',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async execute(setupInfo: ExecuteReq) {
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
if (setupInfo.recoverySource?.type === 'backup') {
|
||||
if (isCifsSource(setupInfo.recoverySource.target)) {
|
||||
setupInfo.recoverySource.target.path =
|
||||
@@ -80,14 +88,14 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.rpcRequest<void>({
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo,
|
||||
})
|
||||
}
|
||||
|
||||
async complete() {
|
||||
const res = await this.rpcRequest<CompleteRes>({
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
const res = await this.rpcRequest<T.SetupResult>({
|
||||
method: 'setup.complete',
|
||||
params: {},
|
||||
})
|
||||
@@ -98,7 +106,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async exit() {
|
||||
async exit(): Promise<void> {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.exit',
|
||||
params: {},
|
||||
@@ -119,7 +127,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
source: CifsRecoverySource | DiskRecoverySource | null,
|
||||
): source is CifsRecoverySource {
|
||||
return !!(source as CifsRecoverySource)?.hostname
|
||||
source: T.BackupTargetFS | null,
|
||||
): source is T.Cifs & { type: 'cifs' } {
|
||||
return !!(source as T.Cifs)?.hostname
|
||||
}
|
||||
|
||||
@@ -1,42 +1,151 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { encodeBase64, pauseFor } from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
AttachReq,
|
||||
CifsRecoverySource,
|
||||
CompleteRes,
|
||||
ExecuteReq,
|
||||
} from './api.service'
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
encodeBase64,
|
||||
pauseFor,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
|
||||
let tries: number
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
Observable,
|
||||
concatMap,
|
||||
delay,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
mergeScan,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
switchScan,
|
||||
takeWhile,
|
||||
} from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
async getStatus() {
|
||||
const restoreOrMigrate = true
|
||||
// fullProgress$(): Observable<T.FullProgress> {
|
||||
// const phases = [
|
||||
// {
|
||||
// name: 'Preparing Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Transferring Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Finalizing Setup',
|
||||
// progress: null,
|
||||
// },
|
||||
// ]
|
||||
|
||||
// return from(phases).pipe(
|
||||
// switchScan((acc, val, i) => {}, { overall: null, phases }),
|
||||
// )
|
||||
// }
|
||||
|
||||
// namedProgress$(namedProgress: T.NamedProgress): Observable<T.NamedProgress> {
|
||||
// return of(namedProgress).pipe(startWith(namedProgress))
|
||||
// }
|
||||
|
||||
// progress$(progress: T.Progress): Observable<T.Progress> {}
|
||||
|
||||
// websocket
|
||||
|
||||
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
|
||||
return of(PROGRESS)
|
||||
// const numPhases = PROGRESS.phases.length
|
||||
|
||||
// return of(PROGRESS).pipe(
|
||||
// switchMap(full =>
|
||||
// from(PROGRESS.phases).pipe(
|
||||
// mergeScan((full, phase, i) => {
|
||||
// if (
|
||||
// !phase.progress ||
|
||||
// typeof phase.progress !== 'object' ||
|
||||
// !phase.progress.total
|
||||
// ) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// return of(full).pipe(delay(2000))
|
||||
// } else {
|
||||
// const total = phase.progress.total
|
||||
// const step = total / 4
|
||||
// let done = phase.progress.done
|
||||
|
||||
// return interval(1000).pipe(
|
||||
// takeWhile(() => done < total),
|
||||
// map(() => {
|
||||
// done += step
|
||||
|
||||
// console.error(done)
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases / 4
|
||||
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// if (done === total) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (i === numPhases - 1) {
|
||||
// full.overall = true
|
||||
// }
|
||||
// }
|
||||
// return full
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
// }, full),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
}
|
||||
|
||||
private statusIndex = 0
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
if (tries === undefined) {
|
||||
tries = 0
|
||||
return null
|
||||
}
|
||||
this.statusIndex++
|
||||
|
||||
tries++
|
||||
|
||||
const total = tries <= 4 ? tries * 268435456 : 1073741824
|
||||
const progress = tries > 4 ? (tries - 4) * 268435456 : 0
|
||||
|
||||
return {
|
||||
bytesTransferred: restoreOrMigrate ? progress : 0,
|
||||
totalBytes: restoreOrMigrate ? total : null,
|
||||
complete: progress === total,
|
||||
switch (this.statusIndex) {
|
||||
case 2:
|
||||
return {
|
||||
status: 'running',
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
status: 'complete',
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getPubKey() {
|
||||
async getPubKey(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
|
||||
// randomly generated
|
||||
@@ -52,7 +161,7 @@ export class MockApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
await pauseFor(1000)
|
||||
return [
|
||||
{
|
||||
@@ -127,7 +236,7 @@ export class MockApiService extends ApiService {
|
||||
]
|
||||
}
|
||||
|
||||
async verifyCifs(params: CifsRecoverySource) {
|
||||
async verifyCifs(params: T.VerifyCifsParams): Promise<StartOSDiskInfo> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
version: '0.3.0',
|
||||
@@ -138,15 +247,25 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async attach(params: AttachReq) {
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async execute(setupInfo: ExecuteReq) {
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async complete(): Promise<CompleteRes> {
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
@@ -155,7 +274,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async exit() {
|
||||
async exit(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
@@ -182,3 +301,8 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX
|
||||
2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W
|
||||
YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const PROGRESS = {
|
||||
overall: null,
|
||||
phases: [],
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ApiService, RecoverySource } from './api/api.service'
|
||||
import { ApiService } from './api/api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService {
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
|
||||
recoverySource?: RecoverySource
|
||||
recoverySource?: T.RecoverySource
|
||||
recoveryPassword?: string
|
||||
|
||||
constructor(private readonly api: ApiService) {}
|
||||
|
||||
@@ -13,6 +13,7 @@ export type LogsRes = {
|
||||
export interface Log {
|
||||
timestamp: string
|
||||
message: string
|
||||
bootId: string
|
||||
}
|
||||
|
||||
export type DiskListResponse = DiskInfo[]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
import { stateNot } from 'src/app/services/state.service'
|
||||
import { AuthGuard } from './guards/auth.guard'
|
||||
import { UnauthGuard } from './guards/unauth.guard'
|
||||
|
||||
@@ -15,15 +16,29 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/login/login.module').then(m => m.LoginPageModule),
|
||||
},
|
||||
{
|
||||
path: 'diagnostic',
|
||||
canActivate: [stateNot(['initializing', 'running'])],
|
||||
loadChildren: () =>
|
||||
import('./pages/diagnostic-routes/diagnostic-routing.module').then(
|
||||
m => m.DiagnosticModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'initializing',
|
||||
canActivate: [stateNot(['error', 'running'])],
|
||||
loadChildren: () =>
|
||||
import('./pages/init/init.module').then(m => m.InitPageModule),
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/server-routes/server-routing.module').then(
|
||||
@@ -32,14 +47,14 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'updates',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/updates/updates.module').then(m => m.UpdatesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'marketplace',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/marketplace-routes/marketplace-routing.module').then(
|
||||
@@ -48,7 +63,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
loadChildren: () =>
|
||||
import('./pages/notifications/notifications.module').then(
|
||||
m => m.NotificationsPageModule,
|
||||
@@ -56,7 +71,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'services',
|
||||
canActivate: [AuthGuard],
|
||||
canActivate: [AuthGuard, stateNot(['error', 'initializing'])],
|
||||
canActivateChild: [AuthGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/apps-routes/apps-routing.module').then(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
type="overlay"
|
||||
side="start"
|
||||
class="left-menu"
|
||||
[class.left-menu_hidden]="withoutMenu"
|
||||
>
|
||||
<ion-content color="light" scrollY="false" class="menu">
|
||||
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
|
||||
|
||||
@@ -9,11 +9,15 @@ tui-root {
|
||||
|
||||
.left-menu {
|
||||
--side-max-width: 280px;
|
||||
|
||||
&_hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
:host-context(body[data-theme='Light']) & {
|
||||
--ion-color-base: #F4F4F5 !important;
|
||||
--ion-color-base: #f4f4f5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { IsActiveMatchOptions, Router } from '@angular/router'
|
||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
@@ -15,6 +16,13 @@ import { THEME } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
|
||||
const OPTIONS: IsActiveMatchOptions = {
|
||||
paths: 'subset',
|
||||
queryParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
@@ -27,7 +35,7 @@ export class AppComponent implements OnDestroy {
|
||||
readonly theme$ = inject(THEME)
|
||||
readonly offline$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.connection.connected$,
|
||||
this.connection$,
|
||||
this.patch
|
||||
.watch$('serverInfo', 'statusInfo')
|
||||
.pipe(startWith({ restarting: false, shuttingDown: false })),
|
||||
@@ -44,8 +52,9 @@ export class AppComponent implements OnDestroy {
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly router: Router,
|
||||
readonly authService: AuthService,
|
||||
readonly connection: ConnectionService,
|
||||
readonly connection$: ConnectionService,
|
||||
readonly clientStorageService: ClientStorageService,
|
||||
readonly themeSwitcher: ThemeSwitcherService,
|
||||
) {}
|
||||
@@ -56,6 +65,13 @@ export class AppComponent implements OnDestroy {
|
||||
.subscribe(name => this.titleService.setTitle(name || 'StartOS'))
|
||||
}
|
||||
|
||||
get withoutMenu(): boolean {
|
||||
return (
|
||||
this.router.isActive('initializing', OPTIONS) ||
|
||||
this.router.isActive('diagnostic', OPTIONS)
|
||||
)
|
||||
}
|
||||
|
||||
splitPaneVisible({ detail }: any) {
|
||||
this.splitPane.sidebarOpen$.next(detail.visible)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
TuiAlertModule,
|
||||
TuiDialogModule,
|
||||
TuiModeModule,
|
||||
TuiRootModule,
|
||||
@@ -58,6 +59,7 @@ import { environment } from '../environments/environment'
|
||||
ConnectionBarComponentModule,
|
||||
TuiRootModule,
|
||||
TuiDialogModule,
|
||||
TuiAlertModule,
|
||||
TuiModeModule,
|
||||
TuiThemeNightModule,
|
||||
WidgetsPageModule,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthService } from './services/auth.service'
|
||||
import { ClientStorageService } from './services/client-storage.service'
|
||||
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
|
||||
import { ThemeSwitcherService } from './services/theme-switcher.service'
|
||||
import { StorageService } from './services/storage.service'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -30,7 +31,7 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [AuthService, ClientStorageService, Router],
|
||||
deps: [StorageService, AuthService, ClientStorageService, Router],
|
||||
useFactory: appInitializer,
|
||||
multi: true,
|
||||
},
|
||||
@@ -45,13 +46,15 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
]
|
||||
|
||||
export function appInitializer(
|
||||
storage: StorageService,
|
||||
auth: AuthService,
|
||||
localStorage: ClientStorageService,
|
||||
router: Router,
|
||||
): () => void {
|
||||
return () => {
|
||||
storage.migrate036()
|
||||
auth.init()
|
||||
localStorage.init()
|
||||
localStorage.init() // @TODO pretty sure we can navigate before this step
|
||||
router.initialNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class MenuComponent {
|
||||
|
||||
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
||||
|
||||
private readonly local$ = this.connectionService.connected$.pipe(
|
||||
private readonly local$ = this.connection$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$('packageData').pipe(first())),
|
||||
switchMap(outer =>
|
||||
@@ -126,6 +126,6 @@ export class MenuComponent {
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly connection$: ConnectionService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, Observable, startWith } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { NetworkService } from 'src/app/services/network.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'connection-bar',
|
||||
@@ -11,16 +12,14 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ConnectionBarComponent {
|
||||
private readonly websocket$ = this.connectionService.websocketConnected$
|
||||
|
||||
readonly connection$: Observable<{
|
||||
message: string
|
||||
color: string
|
||||
icon: string
|
||||
dots: boolean
|
||||
}> = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
this.websocket$.pipe(startWith(false)),
|
||||
this.network$,
|
||||
this.state$.pipe(map(Boolean)),
|
||||
this.patch
|
||||
.watch$('serverInfo', 'statusInfo')
|
||||
.pipe(startWith({ restarting: false, shuttingDown: false })),
|
||||
@@ -65,7 +64,8 @@ export class ConnectionBarComponent {
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly network$: NetworkService,
|
||||
private readonly state$: StateService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import {
|
||||
LogsRes,
|
||||
ServerLogsReq,
|
||||
@@ -72,7 +71,7 @@ export class LogsComponent {
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly downloadHtml: DownloadHTMLService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly connection$: ConnectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -149,43 +148,42 @@ export class LogsComponent {
|
||||
private reconnect$(): Observable<Log[]> {
|
||||
return from(this.followLogs({})).pipe(
|
||||
tap(_ => this.recordConnectionChange()),
|
||||
switchMap(({ guid }) => this.connect$(guid, true)),
|
||||
switchMap(({ guid }) => this.connect$(guid)),
|
||||
)
|
||||
}
|
||||
|
||||
private connect$(guid: string, reconnect = false) {
|
||||
const config: WebSocketSubjectConfig<Log> = {
|
||||
url: `/rpc/${guid}`,
|
||||
openObserver: {
|
||||
next: () => {
|
||||
this.websocketStatus = 'connected'
|
||||
private connect$(guid: string) {
|
||||
return this.api
|
||||
.openWebsocket$<Log>(guid, {
|
||||
openObserver: {
|
||||
next: () => {
|
||||
this.websocketStatus = 'connected'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return this.api.openLogsWebsocket$(config).pipe(
|
||||
tap(_ => this.count++),
|
||||
bufferTime(1000),
|
||||
tap(msgs => {
|
||||
this.loading = false
|
||||
this.processRes({ entries: msgs })
|
||||
if (this.infiniteStatus === 0 && this.count >= this.limit)
|
||||
this.infiniteStatus = 1
|
||||
}),
|
||||
catchError(() => {
|
||||
this.recordConnectionChange(false)
|
||||
return this.connectionService.connected$.pipe(
|
||||
tap(
|
||||
connected =>
|
||||
(this.websocketStatus = connected
|
||||
? 'reconnecting'
|
||||
: 'disconnected'),
|
||||
),
|
||||
filter(Boolean),
|
||||
switchMap(() => this.reconnect$()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.pipe(
|
||||
tap(_ => this.count++),
|
||||
bufferTime(1000),
|
||||
tap(msgs => {
|
||||
this.loading = false
|
||||
this.processRes({ entries: msgs })
|
||||
if (this.infiniteStatus === 0 && this.count >= this.limit)
|
||||
this.infiniteStatus = 1
|
||||
}),
|
||||
catchError(() => {
|
||||
this.recordConnectionChange(false)
|
||||
return this.connection$.pipe(
|
||||
tap(
|
||||
connected =>
|
||||
(this.websocketStatus = connected
|
||||
? 'reconnecting'
|
||||
: 'disconnected'),
|
||||
),
|
||||
filter(Boolean),
|
||||
switchMap(() => this.reconnect$()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private recordConnectionChange(success = true) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<p
|
||||
[style.color]="
|
||||
(connected$ | async) ? 'var(--ion-color-' + rendering.color + ')' : 'gray'
|
||||
(connection$ | async) ? 'var(--ion-color-' + rendering.color + ')' : 'gray'
|
||||
"
|
||||
[style.font-size]="size"
|
||||
[style.font-style]="style"
|
||||
[style.font-weight]="weight"
|
||||
>
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
{{ (connection$ | async) ? rendering.display : 'Unknown' }}
|
||||
|
||||
<span *ngIf="sigtermTimeout && (sigtermTimeout | durationToSeconds) > 30">
|
||||
. This may take a while
|
||||
|
||||
@@ -21,7 +21,5 @@ export class StatusComponent {
|
||||
@Input() installingInfo?: InstallingInfo
|
||||
@Input() sigtermTimeout?: string | null = null
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
constructor(readonly connection$: ConnectionService) {}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,8 @@ export class OSUpdatePage {
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly eosService: EOSService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const releaseNotes = this.eosService.eos?.releaseNotes!
|
||||
const releaseNotes = this.eosService.osUpdate?.releaseNotes!
|
||||
|
||||
this.versions = Object.keys(releaseNotes)
|
||||
.sort()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ng-container *ngIf="connection$ | async; else disconnected">
|
||||
<ion-icon
|
||||
*ngIf="pkg.error; else noError"
|
||||
class="warning-icon"
|
||||
|
||||
@@ -12,7 +12,5 @@ export class AppListIconComponent {
|
||||
@Input()
|
||||
pkg!: PkgInfo
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
constructor(readonly connection$: ConnectionService) {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ng-container *ngIf="!(healthChecks | empty)">
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<!-- connected -->
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ng-container *ngIf="connection$ | async; else disconnected">
|
||||
<ion-item *ngFor="let check of healthChecks | keyvalue">
|
||||
<!-- result -->
|
||||
<ng-container *ngIf="check.value.result as result; else noResult">
|
||||
|
||||
@@ -12,9 +12,7 @@ export class AppShowHealthChecksComponent {
|
||||
@Input()
|
||||
healthChecks!: Record<string, T.HealthCheckResult>
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
constructor(readonly connection$: ConnectionService) {}
|
||||
|
||||
isLoading(result: T.HealthCheckResult['result']): boolean {
|
||||
return result === 'starting' || result === 'loading'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="isInstalled(pkg) && (connected$ | async)">
|
||||
<ng-container *ngIf="isInstalled(pkg) && (connection$ | async)">
|
||||
<ion-grid>
|
||||
<ion-row style="padding-left: 12px">
|
||||
<ion-col>
|
||||
|
||||
@@ -39,8 +39,6 @@ export class AppShowStatusComponent {
|
||||
|
||||
isInstalled = isInstalled
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
@@ -48,7 +46,7 @@ export class AppShowStatusComponent {
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly launcherService: UiLauncherService,
|
||||
private readonly modalService: ModalService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
readonly connection$: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadChildren: () =>
|
||||
import('./logs/logs.module').then(m => m.LogsPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DiagnosticModule {}
|
||||
@@ -1,12 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FormsModule, IonicModule, HomePageRoutingModule],
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule {}
|
||||
@@ -51,12 +51,6 @@
|
||||
}}
|
||||
</ion-button>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertSystemRebuild()" color="warning">
|
||||
System Rebuild
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentAlertRepairDisk()" color="danger">
|
||||
Repair Drive
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
selector: 'diagnostic-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
@@ -25,7 +25,7 @@ export class HomePage {
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const error = await this.api.getError()
|
||||
const error = await this.api.diagnosticGetError()
|
||||
// incorrect drive
|
||||
if (error.code === 15) {
|
||||
this.error = {
|
||||
@@ -92,7 +92,7 @@ export class HomePage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.restart()
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -108,8 +108,8 @@ export class HomePage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.forgetDrive()
|
||||
await this.api.restart()
|
||||
await this.api.diagnosticForgetDrive()
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -118,32 +118,6 @@ export class HomePage {
|
||||
}
|
||||
}
|
||||
|
||||
async presentAlertSystemRebuild() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message:
|
||||
'<p>This action will tear down all service containers and rebuild them from scratch. No data will be deleted.</p><p>A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.</p><p>It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.</p>',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Rebuild',
|
||||
handler: () => {
|
||||
try {
|
||||
this.systemRebuild()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertRepairDisk() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
@@ -174,23 +148,6 @@ export class HomePage {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
private async systemRebuild(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.systemRebuild()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
cssClass: 'loader',
|
||||
@@ -198,8 +155,8 @@ export class HomePage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.repairDisk()
|
||||
await this.api.restart()
|
||||
await this.api.diagnosticRepairDisk()
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService, toLocalIsoString } from '@start9labs/shared'
|
||||
|
||||
var Convert = require('ansi-to-html')
|
||||
@@ -49,7 +49,7 @@ export class LogsPage {
|
||||
|
||||
private async getLogs() {
|
||||
try {
|
||||
const { startCursor, entries } = await this.api.getLogs({
|
||||
const { startCursor, entries } = await this.api.diagnosticGetLogs({
|
||||
cursor: this.startCursor,
|
||||
before: !!this.startCursor,
|
||||
limit: this.limit,
|
||||
24
web/projects/ui/src/app/pages/init/init.module.ts
Normal file
24
web/projects/ui/src/app/pages/init/init.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiProgressModule } from '@taiga-ui/kit'
|
||||
import { LogsModule } from 'src/app/pages/init/logs/logs.module'
|
||||
import { InitPage } from './init.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: InitPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
LogsModule,
|
||||
TuiProgressModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [InitPage],
|
||||
})
|
||||
export class InitPageModule {}
|
||||
18
web/projects/ui/src/app/pages/init/init.page.html
Normal file
18
web/projects/ui/src/app/pages/init/init.page.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<section *ngIf="progress$ | async as progress">
|
||||
<h1 [style.font-size.rem]="2.5" [style.margin.rem]="1">
|
||||
Initializing StartOS
|
||||
</h1>
|
||||
<div class="center-wrapper" *ngIf="progress.total">
|
||||
Progress: {{ (progress.total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
|
||||
<progress
|
||||
tuiProgressBar
|
||||
class="progress"
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress.total"
|
||||
></progress>
|
||||
<p [innerHTML]="progress.message"></p>
|
||||
</section>
|
||||
<logs-window></logs-window>
|
||||
23
web/projects/ui/src/app/pages/init/init.page.scss
Normal file
23
web/projects/ui/src/app/pages/init/init.page.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
section {
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem;
|
||||
text-align: center;
|
||||
/* TODO: Theme */
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
--tui-clear-inverse: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
logs-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 18rem;
|
||||
padding: 1rem;
|
||||
margin: 0 1.5rem auto;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
/* TODO: Theme */
|
||||
background: #181818;
|
||||
}
|
||||
11
web/projects/ui/src/app/pages/init/init.page.ts
Normal file
11
web/projects/ui/src/app/pages/init/init.page.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { InitService } from 'src/app/pages/init/init.service'
|
||||
|
||||
@Component({
|
||||
selector: 'init-page',
|
||||
templateUrl: 'init.page.html',
|
||||
styleUrls: ['init.page.scss'],
|
||||
})
|
||||
export class InitPage {
|
||||
readonly progress$ = inject(InitService)
|
||||
}
|
||||
91
web/projects/ui/src/app/pages/init/init.service.ts
Normal file
91
web/projects/ui/src/app/pages/init/init.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
catchError,
|
||||
defer,
|
||||
EMPTY,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
interface MappedProgress {
|
||||
readonly total: number | null
|
||||
readonly message: string
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class InitService extends Observable<MappedProgress> {
|
||||
private readonly state = inject(StateService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorToastService)
|
||||
private readonly progress$ = defer(() =>
|
||||
from(this.api.initGetProgress()),
|
||||
).pipe(
|
||||
switchMap(({ guid, progress }) =>
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {})
|
||||
.pipe(startWith(progress)),
|
||||
),
|
||||
map(({ phases, overall }) => {
|
||||
return {
|
||||
total: getOverallDecimal(overall),
|
||||
message: phases
|
||||
.filter(
|
||||
(
|
||||
p,
|
||||
): p is {
|
||||
name: string
|
||||
progress: {
|
||||
done: number
|
||||
total: number | null
|
||||
}
|
||||
} => p.progress !== true && p.progress !== null,
|
||||
)
|
||||
.map(p => `<b>${p.name}</b>${getPhaseBytes(p.progress)}`)
|
||||
.join(', '),
|
||||
}
|
||||
}),
|
||||
tap(({ total }) => {
|
||||
if (total === 1) {
|
||||
this.state.syncState()
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
this.errorService.present(e)
|
||||
|
||||
return EMPTY
|
||||
}),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.progress$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
function getOverallDecimal(progress: T.Progress): number {
|
||||
if (progress === true) {
|
||||
return 1
|
||||
} else if (!progress || !progress.total) {
|
||||
return 0
|
||||
} else {
|
||||
return progress.total && progress.done / progress.total
|
||||
}
|
||||
}
|
||||
|
||||
function getPhaseBytes(
|
||||
progress:
|
||||
| false
|
||||
| {
|
||||
done: number
|
||||
total: number | null
|
||||
},
|
||||
): string {
|
||||
return progress === false ? '' : `: (${progress.done}/${progress.total})`
|
||||
}
|
||||
33
web/projects/ui/src/app/pages/init/logs/logs.component.ts
Normal file
33
web/projects/ui/src/app/pages/init/logs/logs.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, ElementRef, inject } from '@angular/core'
|
||||
import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer'
|
||||
import { LogsService } from 'src/app/pages/init/logs/logs.service'
|
||||
|
||||
@Component({
|
||||
selector: 'logs-window',
|
||||
templateUrl: 'logs.template.html',
|
||||
styles: [
|
||||
`
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class LogsComponent {
|
||||
readonly logs$ = inject(LogsService)
|
||||
scroll = true
|
||||
|
||||
scrollTo(bottom: HTMLElement) {
|
||||
if (this.scroll) bottom.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
onBottom([{ isIntersecting }]: readonly IntersectionObserverEntry[]) {
|
||||
this.scroll = isIntersecting
|
||||
}
|
||||
}
|
||||
20
web/projects/ui/src/app/pages/init/logs/logs.module.ts
Normal file
20
web/projects/ui/src/app/pages/init/logs/logs.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IntersectionObserverModule } from '@ng-web-apis/intersection-observer'
|
||||
import { MutationObserverModule } from '@ng-web-apis/mutation-observer'
|
||||
import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { LogsComponent } from './logs.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MutationObserverModule,
|
||||
IntersectionObserverModule,
|
||||
NgDompurifyModule,
|
||||
TuiScrollbarModule,
|
||||
],
|
||||
declarations: [LogsComponent],
|
||||
exports: [LogsComponent],
|
||||
})
|
||||
export class LogsModule {}
|
||||
49
web/projects/ui/src/app/pages/init/logs/logs.service.ts
Normal file
49
web/projects/ui/src/app/pages/init/logs/logs.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Log, toLocalIsoString } from '@start9labs/shared'
|
||||
import {
|
||||
bufferTime,
|
||||
defer,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
scan,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
var Convert = require('ansi-to-html')
|
||||
var convert = new Convert({
|
||||
newline: true,
|
||||
bg: 'transparent',
|
||||
colors: {
|
||||
4: 'Cyan',
|
||||
},
|
||||
escapeXML: true,
|
||||
})
|
||||
|
||||
function convertAnsi(entries: readonly any[]): string {
|
||||
return entries
|
||||
.map(
|
||||
({ timestamp, message }) =>
|
||||
`<b style="color: #FFF">${toLocalIsoString(
|
||||
new Date(timestamp),
|
||||
)}</b> ${convert.toHtml(message)}`,
|
||||
)
|
||||
.join('<br />')
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LogsService extends Observable<readonly string[]> {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly log$ = defer(() => this.api.initFollowLogs({})).pipe(
|
||||
switchMap(({ guid }) => this.api.openWebsocket$<Log>(guid, {})),
|
||||
bufferTime(250),
|
||||
filter(logs => !!logs.length),
|
||||
map(convertAnsi),
|
||||
scan((logs: readonly string[], log) => [...logs, log], []),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.log$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<tui-scrollbar childList subtree (waMutationObserver)="scrollTo(bottom)">
|
||||
<pre *ngFor="let log of logs$ | async" [innerHTML]="log | dompurify"></pre>
|
||||
<section
|
||||
#bottom
|
||||
waIntersectionObserver
|
||||
[style.padding.rem]="1"
|
||||
(waIntersectionObservee)="onBottom($event)"
|
||||
></section>
|
||||
</tui-scrollbar>
|
||||
@@ -42,7 +42,7 @@ export class CAWizardComponent {
|
||||
|
||||
private async testHttps() {
|
||||
const url = `https://${this.document.location.host}${this.relativeUrl}`
|
||||
await this.api.echo({ message: 'ping' }, url).then(() => {
|
||||
await this.api.getState().then(() => {
|
||||
this.caTrusted = true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<ion-item-divider>Memory</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label>Percentage Used</ion-label>
|
||||
<ion-note slot="end">{{ memory.percentageUsed }} %</ion-note>
|
||||
<ion-note slot="end">{{ memory.percentageUsed.value }} %</ion-note>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Total</ion-label>
|
||||
@@ -98,7 +98,7 @@
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>zram Total</ion-label>
|
||||
<ion-note slot="end">{{ memory.zramTotal }} MiB</ion-note>
|
||||
<ion-note slot="end">{{ memory.zramTotal.value }} MiB</ion-note>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>zram Available</ion-label>
|
||||
|
||||
@@ -319,30 +319,6 @@ export class ServerShowPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertSystemRebuild() {
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const minutes = Object.keys(localPkgs).length * 2
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Rebuild',
|
||||
handler: () => {
|
||||
this.systemRebuild()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertRepairDisk() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
@@ -437,23 +413,6 @@ export class ServerShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
private async systemRebuild() {
|
||||
const action = 'System Rebuild'
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: `Beginning ${action}...`,
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.systemRebuild({})
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForEosUpdate(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking for updates',
|
||||
@@ -718,14 +677,6 @@ export class ServerShowPage {
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'System Rebuild',
|
||||
description: '',
|
||||
icon: 'construct-outline',
|
||||
action: () => this.presentAlertSystemRebuild(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Repair Disk',
|
||||
description: '',
|
||||
|
||||
@@ -16,7 +16,7 @@ export module Mock {
|
||||
restarting: false,
|
||||
shuttingDown: false,
|
||||
}
|
||||
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
||||
export const MarketplaceEos: RR.CheckOSUpdateRes = {
|
||||
version: '0.3.5.2',
|
||||
headline: 'Our biggest release ever.',
|
||||
releaseNotes: {
|
||||
@@ -493,30 +493,23 @@ export module Mock {
|
||||
{
|
||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||
message: '****** START *****',
|
||||
bootId: 'hsjnfdklasndhjasvbjamsksajbndjn',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:21:30.872Z',
|
||||
message:
|
||||
'\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.startos/api/graphql \u001b[0;36;49m1.169406ms\u001b',
|
||||
bootId: 'hsjnfdklasndhjasvbjamsksajbndjn',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:22:30.872Z',
|
||||
message: '****** FINISH *****',
|
||||
},
|
||||
]
|
||||
|
||||
export const PackageLogs: Log[] = [
|
||||
{
|
||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||
message: '****** START *****',
|
||||
bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:21:30.872Z',
|
||||
message: 'PackageLogs PackageLogs PackageLogs PackageLogs PackageLogs',
|
||||
},
|
||||
{
|
||||
timestamp: '2019-12-26T14:22:30.872Z',
|
||||
message: '****** FINISH *****',
|
||||
timestamp: '2019-12-26T15:22:30.872Z',
|
||||
message: '****** AGAIN *****',
|
||||
bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export module RR {
|
||||
// websocket
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
// server state
|
||||
|
||||
export type ServerState = 'initializing' | 'error' | 'running'
|
||||
|
||||
// DB
|
||||
|
||||
export type GetRevisionsRes = Revision[] | Dump<DataModel>
|
||||
|
||||
export type GetDumpRes = Dump<DataModel>
|
||||
export type SubscribePatchReq = {}
|
||||
export type SubscribePatchRes = {
|
||||
dump: Dump<DataModel>
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type SetDBValueReq<T> = { pointer: string; value: T } // db.put.ui
|
||||
export type SetDBValueRes = null
|
||||
@@ -33,10 +44,22 @@ export module RR {
|
||||
} // auth.reset-password
|
||||
export type ResetPasswordRes = null
|
||||
|
||||
// server
|
||||
// diagnostic
|
||||
|
||||
export type EchoReq = { message: string; timeout?: number } // server.echo
|
||||
export type EchoRes = string
|
||||
export type DiagnosticErrorRes = {
|
||||
code: number
|
||||
message: string
|
||||
data: { details: string }
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
export type InitGetProgressRes = {
|
||||
progress: T.FullProgress
|
||||
guid: string
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
export type GetSystemTimeReq = {} // server.time
|
||||
export type GetSystemTimeRes = {
|
||||
@@ -65,8 +88,8 @@ export module RR {
|
||||
export type ShutdownServerReq = {} // server.shutdown
|
||||
export type ShutdownServerRes = null
|
||||
|
||||
export type SystemRebuildReq = {} // server.rebuild
|
||||
export type SystemRebuildRes = null
|
||||
export type DiskRepairReq = {} // server.disk.repair
|
||||
export type DiskRepairRes = null
|
||||
|
||||
export type ResetTorReq = {
|
||||
wipeState: boolean
|
||||
@@ -254,8 +277,8 @@ export module RR {
|
||||
export type GetMarketplaceInfoReq = { serverId: string }
|
||||
export type GetMarketplaceInfoRes = StoreInfo
|
||||
|
||||
export type GetMarketplaceEosReq = { serverId: string }
|
||||
export type GetMarketplaceEosRes = MarketplaceEOS
|
||||
export type CheckOSUpdateReq = { serverId: string }
|
||||
export type CheckOSUpdateRes = OSUpdate
|
||||
|
||||
export type GetMarketplacePackagesReq = {
|
||||
ids?: { id: string; version: string }[]
|
||||
@@ -271,7 +294,7 @@ export module RR {
|
||||
export type GetReleaseNotesRes = { [version: string]: string }
|
||||
}
|
||||
|
||||
export interface MarketplaceEOS {
|
||||
export interface OSUpdate {
|
||||
version: string
|
||||
headline: string
|
||||
releaseNotes: { [version: string]: string }
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { RR } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log } from '@start9labs/shared'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export abstract class ApiService {
|
||||
// http
|
||||
@@ -14,8 +10,23 @@ export abstract class ApiService {
|
||||
// for sideloading packages
|
||||
abstract uploadPackage(guid: string, body: Blob): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
abstract openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T>
|
||||
|
||||
// server state
|
||||
|
||||
abstract getState(): Promise<RR.ServerState>
|
||||
|
||||
// db
|
||||
|
||||
abstract subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes>
|
||||
|
||||
abstract setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -35,16 +46,26 @@ export abstract class ApiService {
|
||||
params: RR.ResetPasswordReq,
|
||||
): Promise<RR.ResetPasswordRes>
|
||||
|
||||
// diagnostic
|
||||
|
||||
abstract diagnosticGetError(): Promise<RR.DiagnosticErrorRes>
|
||||
abstract diagnosticRestart(): Promise<void>
|
||||
abstract diagnosticForgetDrive(): Promise<void>
|
||||
abstract diagnosticRepairDisk(): Promise<void>
|
||||
abstract diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
// init
|
||||
|
||||
abstract initGetProgress(): Promise<RR.InitGetProgressRes>
|
||||
|
||||
abstract initFollowLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
// server
|
||||
|
||||
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
|
||||
abstract getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes>
|
||||
@@ -89,11 +110,7 @@ export abstract class ApiService {
|
||||
params: RR.ShutdownServerReq,
|
||||
): Promise<RR.ShutdownServerRes>
|
||||
|
||||
abstract systemRebuild(
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes>
|
||||
|
||||
abstract repairDisk(params: RR.SystemRebuildReq): Promise<RR.SystemRebuildRes>
|
||||
abstract repairDisk(params: RR.DiskRepairReq): Promise<RR.DiskRepairRes>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
@@ -105,7 +122,7 @@ export abstract class ApiService {
|
||||
url: string,
|
||||
): Promise<T>
|
||||
|
||||
abstract getEos(): Promise<RR.GetMarketplaceEosRes>
|
||||
abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes>
|
||||
|
||||
// notification
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Log,
|
||||
Method,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
@@ -12,13 +11,12 @@ import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import { PatchDB, pathFromArray } from 'patch-db-client'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
@@ -30,10 +28,11 @@ export class LiveApiService extends ApiService {
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {
|
||||
super()
|
||||
;(window as any).rpcClient = this
|
||||
; (window as any).rpcClient = this
|
||||
}
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
@@ -43,6 +42,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
// for sideloading packages
|
||||
|
||||
async uploadPackage(guid: string, body: Blob): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.POST,
|
||||
@@ -52,8 +52,36 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
|
||||
return webSocket({
|
||||
url: `${protocol}://${host}/ws/rpc/${guid}`,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// state
|
||||
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
return this.rpcRequest({ method: 'state', params: {} })
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes> {
|
||||
return this.rpcRequest({ method: 'db.subscribe', params })
|
||||
}
|
||||
|
||||
async setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -87,29 +115,57 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'auth.reset-password', params })
|
||||
}
|
||||
|
||||
// diagnostic
|
||||
|
||||
async diagnosticGetError(): Promise<RR.DiagnosticErrorRes> {
|
||||
return this.rpcRequest<RR.DiagnosticErrorRes>({
|
||||
method: 'diagnostic.error',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticRestart(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticForgetDrive(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.forget',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticRepairDisk(): Promise<void> {
|
||||
return this.rpcRequest<void>({
|
||||
method: 'diagnostic.disk.repair',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.rpcRequest<RR.GetServerLogsRes>({
|
||||
method: 'diagnostic.logs',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
async initGetProgress(): Promise<RR.InitGetProgressRes> {
|
||||
return this.rpcRequest({ method: 'init.subscribe', params: {} })
|
||||
}
|
||||
|
||||
async initFollowLogs(): Promise<RR.FollowServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'init.logs.follow', params: {} })
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, urlOverride)
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
const config: WebSocketSubjectConfig<Update<DataModel>> = {
|
||||
url: `/db`,
|
||||
closeObserver: {
|
||||
next: val => {
|
||||
if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
async getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes> {
|
||||
@@ -175,12 +231,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.shutdown', params })
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
return this.rpcRequest({ method: 'server.rebuild', params })
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.rpcRequest({ method: 'disk.repair', params })
|
||||
}
|
||||
@@ -203,10 +253,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getEos(): Promise<RR.GetMarketplaceEosRes> {
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
const qp: RR.GetMarketplaceEosReq = { serverId: id }
|
||||
|
||||
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
|
||||
return this.marketplaceProxy(
|
||||
'/eos/v0/latest',
|
||||
qp,
|
||||
@@ -417,16 +464,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
|
||||
config.url = `${protocol}://${host}/ws${config.url}`
|
||||
|
||||
return webSocket(config)
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
urlOverride?: string,
|
||||
@@ -445,9 +482,7 @@ export class LiveApiService extends ApiService {
|
||||
const patchSequence = res.headers.get('x-patch-sequence')
|
||||
if (patchSequence)
|
||||
await firstValueFrom(
|
||||
this.patch.cache$.pipe(
|
||||
filter(({ sequence }) => sequence >= Number(patchSequence)),
|
||||
),
|
||||
this.patch.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))),
|
||||
)
|
||||
|
||||
return body.result
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Log, pauseFor } from '@start9labs/shared'
|
||||
import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import {
|
||||
Operation,
|
||||
PatchOp,
|
||||
pathFromArray,
|
||||
RemoveOperation,
|
||||
Update,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
InstallingState,
|
||||
PackageDataEntry,
|
||||
StateInfo,
|
||||
@@ -20,22 +19,17 @@ import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import {
|
||||
EMPTY,
|
||||
iif,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { StoreInfo } from '@start9labs/marketplace'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@@ -71,32 +65,17 @@ const PROGRESS: T.FullProgress = {
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockWsSource$ = new Subject<Update<DataModel>>()
|
||||
readonly mockWsSource$ = new Subject<Revision>()
|
||||
private readonly revertTime = 1800
|
||||
sequence = 0
|
||||
|
||||
constructor(
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly auth: AuthService,
|
||||
) {
|
||||
constructor(private readonly auth: AuthService) {
|
||||
super()
|
||||
this.auth.isVerified$
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.sequence = 0
|
||||
}),
|
||||
switchMap(verified =>
|
||||
iif(
|
||||
() => verified,
|
||||
timer(2000).pipe(
|
||||
tap(() => {
|
||||
this.connectionService.websocketConnected$.next(true)
|
||||
}),
|
||||
),
|
||||
EMPTY,
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
@@ -111,8 +90,57 @@ export class MockApiService extends ApiService {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T>,
|
||||
): Observable<T> {
|
||||
if (guid === 'db-guid') {
|
||||
return this.mockWsSource$.pipe<any>(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
} else if (guid === 'logs-guid') {
|
||||
return interval(50).pipe<any>(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
} else if (guid === 'init-progress-guid') {
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
} else {
|
||||
throw new Error('invalid guid type')
|
||||
}
|
||||
}
|
||||
|
||||
// server state
|
||||
|
||||
private stateIndex = 0
|
||||
async getState(): Promise<RR.ServerState> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.stateIndex++
|
||||
|
||||
return this.stateIndex === 1 ? 'initializing' : 'running'
|
||||
}
|
||||
|
||||
// db
|
||||
|
||||
async subscribeToPatchDB(
|
||||
params: RR.SubscribePatchReq,
|
||||
): Promise<RR.SubscribePatchRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
dump: { id: 1, value: mockPatchData },
|
||||
guid: 'db-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async setDbValue<T>(
|
||||
pathArr: Array<string | number>,
|
||||
value: T,
|
||||
@@ -136,11 +164,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(() => {
|
||||
this.mockWsSource$.next({ id: 1, value: mockPatchData })
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -166,34 +189,63 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// server
|
||||
// diagnostic
|
||||
|
||||
async echo(params: RR.EchoReq, url?: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
async getError(): Promise<RPCErrorDetails> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async diagnosticGetError(): Promise<RR.DiagnosticErrorRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
code: 15,
|
||||
message: 'Unknown server',
|
||||
data: { details: 'Some details about the error here' },
|
||||
}
|
||||
}
|
||||
|
||||
async diagnosticRestart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticForgetDrive(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticRepairDisk(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async diagnosticGetLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.getServerLogs(params)
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
async initGetProgress(): Promise<RR.InitGetProgressRes> {
|
||||
await pauseFor(250)
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'init-progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async initFollowLogs(): Promise<RR.FollowServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
return this.mockWsSource$.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(50).pipe(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
}
|
||||
// server
|
||||
|
||||
async getSystemTime(
|
||||
params: RR.GetSystemTimeReq,
|
||||
@@ -248,7 +300,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +310,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,11 +320,11 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
randomLogs(limit = 1): Log[] {
|
||||
private randomLogs(limit = 1): Log[] {
|
||||
const arrLength = Math.ceil(limit / Mock.ServerLogs.length)
|
||||
const logs = new Array(arrLength)
|
||||
.fill(Mock.ServerLogs)
|
||||
@@ -374,12 +426,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes> {
|
||||
return this.restartServer(params)
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
@@ -422,7 +468,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async getEos(): Promise<RR.GetMarketplaceEosRes> {
|
||||
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.MarketplaceEos
|
||||
}
|
||||
@@ -641,13 +687,13 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
let entries
|
||||
if (Math.random() < 0.2) {
|
||||
entries = Mock.PackageLogs
|
||||
entries = Mock.ServerLogs
|
||||
} else {
|
||||
const arrLength = params.limit
|
||||
? Math.ceil(params.limit / Mock.PackageLogs.length)
|
||||
? Math.ceil(params.limit / Mock.ServerLogs.length)
|
||||
: 10
|
||||
entries = new Array(arrLength)
|
||||
.fill(Mock.PackageLogs)
|
||||
.fill(Mock.ServerLogs)
|
||||
.reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
return {
|
||||
@@ -663,7 +709,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: '7251d5be-645f-4362-a51b-3a85be92b31e',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +719,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id)
|
||||
this.installProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const patch: Operation<
|
||||
@@ -745,7 +791,7 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(id)
|
||||
this.installProgress(id)
|
||||
}, 2000)
|
||||
|
||||
return {
|
||||
@@ -1013,7 +1059,57 @@ export class MockApiService extends ApiService {
|
||||
return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated
|
||||
}
|
||||
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
private async initProgress(): Promise<T.FullProgress> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
for (let [i, phase] of progress.phases.entries()) {
|
||||
if (
|
||||
!phase.progress ||
|
||||
typeof phase.progress !== 'object' ||
|
||||
!phase.progress.total
|
||||
) {
|
||||
await pauseFor(2000)
|
||||
|
||||
progress.phases[i].progress = true
|
||||
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length
|
||||
progress.overall.done += step
|
||||
}
|
||||
} else {
|
||||
const step = phase.progress.total / 4
|
||||
|
||||
while (phase.progress.done < phase.progress.total) {
|
||||
await pauseFor(200)
|
||||
|
||||
phase.progress.done += step
|
||||
|
||||
if (
|
||||
progress.overall &&
|
||||
typeof progress.overall === 'object' &&
|
||||
progress.overall.total
|
||||
) {
|
||||
const step = progress.overall.total / progress.phases.length / 4
|
||||
|
||||
progress.overall.done += step
|
||||
}
|
||||
|
||||
if (phase.progress.done === phase.progress.total) {
|
||||
await pauseFor(250)
|
||||
|
||||
progress.phases[i].progress = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
private async installProgress(id: string): Promise<void> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
for (let [i, phase] of progress.phases.entries()) {
|
||||
@@ -1194,10 +1290,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
private async mockRevision<T>(patch: Operation<T>[]): Promise<void> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
}
|
||||
const revision = {
|
||||
id: ++this.sequence,
|
||||
patch,
|
||||
|
||||
@@ -12,7 +12,7 @@ export enum AuthState {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly LOGGED_IN_KEY = 'loggedIn'
|
||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||
|
||||
readonly isVerified$ = this.authState$.pipe(
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { combineLatest, Observable, shareReplay } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { NetworkService } from 'src/app/services/network.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
readonly networkConnected$ = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => navigator.onLine),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
readonly websocketConnected$ = new ReplaySubject<boolean>(1)
|
||||
readonly connected$ = combineLatest([
|
||||
this.networkConnected$,
|
||||
this.websocketConnected$.pipe(distinctUntilChanged()),
|
||||
export class ConnectionService extends Observable<boolean> {
|
||||
private readonly stream$ = combineLatest([
|
||||
inject(NetworkService),
|
||||
inject(StateService).pipe(map(Boolean)),
|
||||
]).pipe(
|
||||
map(([network, websocket]) => network && websocket),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
||||
import { OSUpdate } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
@@ -12,7 +12,7 @@ import { DataModel } from './patch-db/data-model'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EOSService {
|
||||
eos?: MarketplaceEOS
|
||||
osUpdate?: OSUpdate
|
||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe(
|
||||
@@ -52,9 +52,10 @@ export class EOSService {
|
||||
) {}
|
||||
|
||||
async loadEos(): Promise<void> {
|
||||
const { version } = await getServerInfo(this.patch)
|
||||
this.eos = await this.api.getEos()
|
||||
const updateAvailable = this.emver.compare(this.eos.version, version) === 1
|
||||
const { version, id } = await getServerInfo(this.patch)
|
||||
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
|
||||
const updateAvailable =
|
||||
this.emver.compare(this.osUpdate.version, version) === 1
|
||||
this.updateAvailable$.next(updateAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
22
web/projects/ui/src/app/services/network.service.ts
Normal file
22
web/projects/ui/src/app/services/network.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { fromEvent, merge, Observable, shareReplay } from 'rxjs'
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkService extends Observable<boolean> {
|
||||
private readonly win = inject(WINDOW)
|
||||
private readonly stream$ = merge(
|
||||
fromEvent(this.win, 'online'),
|
||||
fromEvent(this.win, 'offline'),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => this.win.navigator.onLine),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { filter, map, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -11,21 +11,25 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
|
||||
// Get data from PatchDb after is starts and act upon it
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||
export class PatchDataService extends Observable<void> {
|
||||
private readonly stream$ = this.connection$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$()),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to eOS and services
|
||||
this.checkForUpdates()
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui.ackWelcome)
|
||||
map((cache, index) => {
|
||||
this.bootstrapper.update(cache)
|
||||
|
||||
if (index === 0) {
|
||||
// check for updates to StartOS and services
|
||||
this.checkForUpdates()
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(cache.ui.ackWelcome)
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
@@ -38,7 +42,8 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly embassyApi: ApiService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly connection$: ConnectionService,
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bootstrapper, DBCache } from 'patch-db-client'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { StorageService } from '../storage.service'
|
||||
@@ -6,20 +6,18 @@ import { StorageService } from '../storage.service'
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LocalStorageBootstrap implements Bootstrapper<DataModel> {
|
||||
static CONTENT_KEY = 'patch-db-cache'
|
||||
export class LocalStorageBootstrap {
|
||||
static CONTENT_KEY = 'patchDB'
|
||||
|
||||
constructor(private readonly storage: StorageService) {}
|
||||
|
||||
init(): DBCache<DataModel> {
|
||||
const cache = this.storage.get<DBCache<DataModel>>(
|
||||
LocalStorageBootstrap.CONTENT_KEY,
|
||||
)
|
||||
init(): Dump<DataModel> {
|
||||
const cache = this.storage.get<DataModel>(LocalStorageBootstrap.CONTENT_KEY)
|
||||
|
||||
return cache || { sequence: 0, data: {} as DataModel }
|
||||
return cache ? { id: 1, value: cache } : { id: 0, value: {} as DataModel }
|
||||
}
|
||||
|
||||
update(cache: DBCache<DataModel>): void {
|
||||
update(cache: DataModel): void {
|
||||
this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { InjectionToken, Injector } from '@angular/core'
|
||||
import { Revision, Update } from 'patch-db-client'
|
||||
import { defer, EMPTY, from, Observable } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
filter,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { DataModel } from './data-model'
|
||||
import { defer, EMPTY, from, interval, Observable } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DataModel } from './data-model'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
|
||||
'',
|
||||
@@ -25,33 +25,31 @@ export function sourceFactory(
|
||||
// defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there
|
||||
return defer(() => {
|
||||
const api = injector.get(ApiService)
|
||||
const authService = injector.get(AuthService)
|
||||
const connectionService = injector.get(ConnectionService)
|
||||
const configService = injector.get(ConfigService)
|
||||
const isTor = configService.isTor()
|
||||
const timeout = isTor ? 16000 : 4000
|
||||
const auth = injector.get(AuthService)
|
||||
const state = injector.get(StateService)
|
||||
const bootstrapper = injector.get(LocalStorageBootstrap)
|
||||
|
||||
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||
bufferTime(250),
|
||||
filter(updates => !!updates.length),
|
||||
catchError((_, watch$) => {
|
||||
connectionService.websocketConnected$.next(false)
|
||||
return auth.isVerified$.pipe(
|
||||
switchMap(verified =>
|
||||
verified ? from(api.subscribeToPatchDB({})) : EMPTY,
|
||||
),
|
||||
switchMap(({ dump, guid }) =>
|
||||
api.openWebsocket$<Revision>(guid, {}).pipe(
|
||||
bufferTime(250),
|
||||
filter(revisions => !!revisions.length),
|
||||
startWith([dump]),
|
||||
),
|
||||
),
|
||||
catchError((_, original$) => {
|
||||
state.retrigger()
|
||||
|
||||
return interval(timeout).pipe(
|
||||
switchMap(() =>
|
||||
from(api.echo({ message: 'ping', timeout })).pipe(
|
||||
catchError(() => EMPTY),
|
||||
),
|
||||
),
|
||||
return state.pipe(
|
||||
filter(current => current === 'running'),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
switchMap(() => original$),
|
||||
)
|
||||
}),
|
||||
tap(() => connectionService.websocketConnected$.next(true)),
|
||||
)
|
||||
|
||||
return authService.isVerified$.pipe(
|
||||
switchMap(verified => (verified ? websocket$ : EMPTY)),
|
||||
startWith([bootstrapper.init()]),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,24 +4,19 @@ import { tap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
|
||||
// Start and stop PatchDb upon verification
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchMonitorService extends Observable<any> {
|
||||
// @TODO not happy with Observable<void>
|
||||
export class PatchMonitorService extends Observable<unknown> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
tap(verified =>
|
||||
verified ? this.patch.start(this.bootstrapper) : this.patch.stop(),
|
||||
),
|
||||
tap(verified => (verified ? this.patch.start() : this.patch.stop())),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
136
web/projects/ui/src/app/services/state.service.ts
Normal file
136
web/projects/ui/src/app/services/state.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
|
||||
import { ALWAYS_TRUE_HANDLER } from '@taiga-ui/cdk'
|
||||
import { TuiAlertService, TuiNotification } from '@taiga-ui/core'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concat,
|
||||
EMPTY,
|
||||
exhaustMap,
|
||||
from,
|
||||
merge,
|
||||
Observable,
|
||||
startWith,
|
||||
Subject,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
skip,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { NetworkService } from 'src/app/services/network.service'
|
||||
|
||||
const OPTIONS: IsActiveMatchOptions = {
|
||||
paths: 'subset',
|
||||
queryParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService extends Observable<RR.ServerState | null> {
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly network$ = inject(NetworkService)
|
||||
|
||||
private readonly single$ = new Subject<RR.ServerState>()
|
||||
|
||||
private readonly trigger$ = new BehaviorSubject<void>(undefined)
|
||||
private readonly poll$ = this.trigger$.pipe(
|
||||
switchMap(() =>
|
||||
timer(0, 2000).pipe(
|
||||
switchMap(() =>
|
||||
from(this.api.getState()).pipe(catchError(() => EMPTY)),
|
||||
),
|
||||
take(1),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private readonly stream$ = merge(this.single$, this.poll$).pipe(
|
||||
tap(state => {
|
||||
switch (state) {
|
||||
case 'initializing':
|
||||
this.router.navigate(['initializing'], { replaceUrl: true })
|
||||
break
|
||||
case 'error':
|
||||
this.router.navigate(['diagnostic'], { replaceUrl: true })
|
||||
break
|
||||
case 'running':
|
||||
if (
|
||||
this.router.isActive('initializing', OPTIONS) ||
|
||||
this.router.isActive('diagnostic', OPTIONS)
|
||||
) {
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}),
|
||||
startWith(null),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly alert = merge(
|
||||
this.trigger$.pipe(skip(1)),
|
||||
this.network$.pipe(filter(v => !v)),
|
||||
)
|
||||
.pipe(
|
||||
exhaustMap(() =>
|
||||
concat(
|
||||
this.alerts
|
||||
.open('Trying to reach server', {
|
||||
label: 'State unknown',
|
||||
autoClose: false,
|
||||
status: TuiNotification.Error,
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(
|
||||
combineLatest([this.stream$, this.network$]).pipe(
|
||||
filter(state => state.every(Boolean)),
|
||||
),
|
||||
),
|
||||
),
|
||||
this.alerts.open('Connection restored', {
|
||||
label: 'Server reached',
|
||||
status: TuiNotification.Success,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe() // @TODO shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe?
|
||||
|
||||
constructor() {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
retrigger() {
|
||||
this.trigger$.next()
|
||||
}
|
||||
|
||||
async syncState() {
|
||||
const state = await this.api.getState()
|
||||
this.single$.next(state)
|
||||
}
|
||||
}
|
||||
|
||||
export function stateNot(state: RR.ServerState[]): CanActivateFn {
|
||||
return () =>
|
||||
inject(StateService).pipe(
|
||||
filter(current => !current || !state.includes(current)),
|
||||
map(ALWAYS_TRUE_HANDLER),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
const PREFIX = '_embassystorage/_embassykv/'
|
||||
const PREFIX = '_startos/'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -15,16 +15,21 @@ export class StorageService {
|
||||
return JSON.parse(String(this.storage.getItem(`${PREFIX}${key}`)))
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T) {
|
||||
set(key: string, value: any) {
|
||||
this.storage.setItem(`${PREFIX}${key}`, JSON.stringify(value))
|
||||
}
|
||||
|
||||
clear() {
|
||||
Array.from(
|
||||
{ length: this.storage.length },
|
||||
(_, i) => this.storage.key(i) || '',
|
||||
)
|
||||
.filter(key => key.startsWith(PREFIX))
|
||||
.forEach(key => this.storage.removeItem(key))
|
||||
this.storage.clear()
|
||||
}
|
||||
|
||||
migrate036() {
|
||||
const oldPrefix = '_embassystorage/_embassykv/'
|
||||
if (!!this.storage.getItem(`${oldPrefix}loggedInKey`)) {
|
||||
const cache = this.storage.getItem(`${oldPrefix}patch-db-cache`)
|
||||
this.clear()
|
||||
this.set('loggedIn', true)
|
||||
this.set('patchDB', cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user