mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
merge from master and fix typescript errors
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"useMocks": true,
|
||||
"enableWidgets": false,
|
||||
"packageArch": "aarch64",
|
||||
"osArch": "raspberrypi",
|
||||
"ui": {
|
||||
"api": {
|
||||
"url": "rpc",
|
||||
|
||||
13319
frontend/package-lock.json
generated
13319
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -47,8 +47,9 @@
|
||||
"@maskito/angular": "^0.10.0",
|
||||
"@maskito/core": "^0.10.0",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc5",
|
||||
"@taiga-ui/addon-charts": "3.28.0",
|
||||
"@taiga-ui/cdk": "3.28.0",
|
||||
"@taiga-ui/core": "3.28.0",
|
||||
@@ -76,7 +77,6 @@
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc5",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
@@ -115,4 +115,4 @@
|
||||
"pre-commit": "lint-staged --concurrent false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
[type]="!unmasked1 ? 'password' : 'text'"
|
||||
placeholder="Enter Password"
|
||||
(ionChange)="validate()"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="unmasked1 = !unmasked1">
|
||||
<ion-icon
|
||||
@@ -48,7 +47,6 @@
|
||||
[type]="!unmasked2 ? 'password' : 'text'"
|
||||
placeholder="Retype Password"
|
||||
(ionChange)="checkVer()"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<p>
|
||||
Download your server's Root CA and
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
@@ -104,7 +104,7 @@
|
||||
<span style="font-weight: bold">Note:</span>
|
||||
This address will only work from a Tor-enabled browser.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/connecting-tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
|
||||
@@ -8,23 +8,21 @@
|
||||
<ion-card>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col responsiveCol sizeXs="12" class="ion-text-center">
|
||||
<div class="inline" style="margin-bottom: 3rem">
|
||||
<div class="inline mb-12">
|
||||
<ion-icon
|
||||
name="checkmark-circle-outline"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<h1>Setup Complete!</h1>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<ion-card id="exit" (click)="exitKiosk()">
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Continue to login</p>
|
||||
<ion-icon name="log-in-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</ion-card>
|
||||
</div>
|
||||
<ion-button
|
||||
shape="round"
|
||||
class="login-button mb-12"
|
||||
(click)="exitKiosk()"
|
||||
>
|
||||
Continue to Login
|
||||
<ion-icon name="log-in-outline" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-card>
|
||||
@@ -34,8 +32,8 @@
|
||||
<ion-card *ngIf="lanAddress">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col responsiveCol sizeXs="12" class="ion-text-center">
|
||||
<div style="margin-bottom: 4rem">
|
||||
<div class="inline">
|
||||
<div class="mb-12">
|
||||
<div class="inline-container setup">
|
||||
<ion-icon
|
||||
name="checkmark-circle-outline"
|
||||
color="success"
|
||||
@@ -52,35 +50,40 @@
|
||||
<div class="card-container">
|
||||
<ion-card id="information" (click)="download()">
|
||||
<ion-card-content>
|
||||
<ion-card-title>
|
||||
Download permanent address info
|
||||
</ion-card-title>
|
||||
<ion-card-title>Download address info</ion-card-title>
|
||||
<p>
|
||||
start.local was for setup purposes only. It will no
|
||||
longer work.
|
||||
</p>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Download</p>
|
||||
<ion-icon name="download-outline"></ion-icon>
|
||||
</div>
|
||||
<div class="inline-container">
|
||||
<p class="action-text">Download</p>
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</div>
|
||||
</ion-footer>
|
||||
</ion-card>
|
||||
<ion-card
|
||||
[disabled]="disableLogin"
|
||||
id="launch"
|
||||
[disabled]="disableLogin"
|
||||
href="{{ lanAddress }}"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Login to StartOS</p>
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
<ion-card-content>
|
||||
<ion-card-title>Trust your Root CA</ion-card-title>
|
||||
<p>
|
||||
In the new tab, follow instructions to trust your
|
||||
server's Root CA and log in.
|
||||
</p>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<div class="container">
|
||||
<div class="inline-container">
|
||||
<p class="action-text">Open</p>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-footer>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ion-col>
|
||||
|
||||
@@ -18,19 +18,24 @@ ion-content {
|
||||
|
||||
ion-grid {
|
||||
max-width: 760px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-center-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.inline-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
padding: 3rem;
|
||||
padding: 2.4rem;
|
||||
|
||||
h1 {
|
||||
color: var(--ion-color-success);
|
||||
@@ -44,14 +49,14 @@ ion-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
// download info card
|
||||
ion-card {
|
||||
max-width: 91%;
|
||||
min-width: 91%;
|
||||
min-height: 260px;
|
||||
width: 80%;
|
||||
background: #615F5F;
|
||||
color: var(--ion-text-color);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
margin: auto;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@@ -70,14 +75,6 @@ ion-card {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
padding-bottom: 4rem;
|
||||
|
||||
p {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
@@ -100,19 +97,24 @@ ion-card {
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#exit {
|
||||
background: var(--color-accent);
|
||||
height: 100%;
|
||||
|
||||
.container p {
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
.login-button {
|
||||
--background: var(--color-accent);
|
||||
--padding-bottom: 2.5rem;
|
||||
--padding-top: 2.5rem;
|
||||
--padding-start: 2.5rem;
|
||||
--padding-end: 2.5rem;
|
||||
--border-radius: 44px;
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
transition: all 350ms ease;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transition-property: transform;
|
||||
transform: scale(1.05);
|
||||
transition-delay: 40ms;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
@@ -120,40 +122,62 @@ ion-card {
|
||||
}
|
||||
}
|
||||
|
||||
#launch {
|
||||
background: var(--alt-blue);
|
||||
height: 100%;
|
||||
|
||||
.container p {
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
.launch-button {
|
||||
--background: var(--alt-blue);
|
||||
}
|
||||
|
||||
#information:after {
|
||||
#information:after, #launch:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 80%;
|
||||
top: 79%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
#launch:after {
|
||||
background: var(--alt-blue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
.mb-12 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.emphasis-warn {
|
||||
font-weight: 600;
|
||||
color: var(--ion-color-warning);
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.action-text {
|
||||
font-variant-caps: all-small-caps;
|
||||
padding-right: 0.5rem;
|
||||
font-size: 1.5rem !important;
|
||||
letter-spacing: 0.03rem;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.setup {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
ion-card {
|
||||
width: 100%;
|
||||
padding-bottom: unset;
|
||||
}
|
||||
#information:after {
|
||||
top: 84%;
|
||||
}
|
||||
#launch:after {
|
||||
top: 85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,18 +45,7 @@ export class SuccessPage {
|
||||
|
||||
async ngAfterViewInit() {
|
||||
this.ngZone.runOutsideAngular(() => this.initMatrix())
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
|
||||
this.cert = ret['root-ca']
|
||||
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
await this.errCtrl.present(e)
|
||||
}
|
||||
setTimeout(() => this.complete(), 1000)
|
||||
}
|
||||
|
||||
download() {
|
||||
@@ -83,6 +72,21 @@ export class SuccessPage {
|
||||
this.api.exit()
|
||||
}
|
||||
|
||||
private async complete() {
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
this.torAddress = ret['tor-address'].replace(/^https:/, 'http:')
|
||||
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
|
||||
this.cert = ret['root-ca']
|
||||
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
await this.errCtrl.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
private initMatrix() {
|
||||
this.ctx = this.canvas.nativeElement.getContext('2d')!
|
||||
this.canvas.nativeElement.width = window.innerWidth
|
||||
|
||||
@@ -218,7 +218,7 @@ ion-toast {
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.3rem;
|
||||
padding-left: 0px 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, HostListener, Inject } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { debounce } from '@start9labs/shared'
|
||||
import { debounce } from '../../util/misc.util'
|
||||
|
||||
@Directive({
|
||||
selector: '[appEnter]',
|
||||
@@ -27,6 +27,8 @@ export * from './directives/responsive-col/responsive-col.module'
|
||||
export * from './directives/responsive-col/responsive-col-viewport.directive'
|
||||
export * from './directives/safe-links/safe-links.directive'
|
||||
export * from './directives/safe-links/safe-links.module'
|
||||
export * from './directives/enter/enter.directive'
|
||||
export * from './directives/enter/enter.module'
|
||||
|
||||
export * from './mocks/get-setup-status'
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ export function getErrorMessage(
|
||||
} else if (e.code === 0) {
|
||||
message =
|
||||
'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again'
|
||||
link = 'https://docs.start9.com/0.3.5.x/support/common-issues#request-error'
|
||||
} else if (!e.message) {
|
||||
message = 'Unknown Error'
|
||||
link = 'https://docs.start9.com/latest/support/faq'
|
||||
} else {
|
||||
message = e.message
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export type WorkspaceConfig = {
|
||||
packageArch: 'aarch64' | 'x86_64'
|
||||
osArch: 'aarch64' | 'x86_64' | 'raspberrypi'
|
||||
gitHash: string
|
||||
useMocks: boolean
|
||||
enableWidgets: boolean
|
||||
|
||||
@@ -26,10 +26,7 @@
|
||||
type="overlay"
|
||||
side="end"
|
||||
class="right-menu container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
[class.container_offline]="offline$ | async"
|
||||
[class.right-menu_hidden]="!drawer.open"
|
||||
[style.--side-width.px]="drawer.width"
|
||||
>
|
||||
@@ -47,10 +44,7 @@
|
||||
[responsiveColViewport]="viewport"
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
[class.container_offline]="offline$ | async"
|
||||
>
|
||||
<ion-content
|
||||
#viewport="viewport"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { merge } from 'rxjs'
|
||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
@@ -25,6 +25,19 @@ export class AppComponent implements OnDestroy {
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
readonly theme$ = inject(THEME)
|
||||
readonly offline$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.connection.connected$,
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(
|
||||
([verified, connected, status]) =>
|
||||
verified &&
|
||||
(!connected || status.restarting || status['shutting-down']),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly titleService: Title,
|
||||
|
||||
@@ -11,11 +11,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import {
|
||||
MarkdownModule,
|
||||
DarkThemeModule,
|
||||
EnterModule,
|
||||
LightThemeModule,
|
||||
MarkdownModule,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
LightThemeModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
@@ -24,7 +25,6 @@ import { OSWelcomePageModule } from './common/os-welcome/os-welcome.module'
|
||||
import { PreloaderModule } from './app/preloader/preloader.module'
|
||||
import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
import { EnterModule } from './app/enter/enter.module'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||
import { ToastContainerModule } from './common/toast-container/toast-container.module'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'connection-bar',
|
||||
@@ -19,8 +21,11 @@ export class ConnectionBarComponent {
|
||||
}> = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
this.websocket$.pipe(startWith(false)),
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(([network, websocket]) => {
|
||||
map(([network, websocket, status]) => {
|
||||
if (!network)
|
||||
return {
|
||||
message: 'No Internet',
|
||||
@@ -35,6 +40,20 @@ export class ConnectionBarComponent {
|
||||
icon: 'cloud-offline-outline',
|
||||
dots: true,
|
||||
}
|
||||
if (status['shutting-down'])
|
||||
return {
|
||||
message: 'Shutting Down',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
if (status.restarting)
|
||||
return {
|
||||
message: 'Restarting',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Connected',
|
||||
@@ -45,5 +64,8 @@ export class ConnectionBarComponent {
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,17 @@
|
||||
<ion-label class="label montserrat" routerLinkActive="label_selected">
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (warning$ | async)"
|
||||
color="warning"
|
||||
size="small"
|
||||
name="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (showEOSUpdate$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket-outline"
|
||||
name="rocket"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*ngIf="page.url === '/updates' && (updateCount$ | async) as updateCount"
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
pairwise,
|
||||
startWith,
|
||||
switchMap,
|
||||
@@ -22,6 +24,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { Emver, THEME } from '@start9labs/shared'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
@@ -112,6 +115,11 @@ export class MenuComponent {
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
readonly warning$ = merge(
|
||||
of(this.config.isTorHttp()),
|
||||
this.patch.watch$('server-info', 'ntp-synced').pipe(map(synced => !synced)),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly eosService: EOSService,
|
||||
@@ -120,5 +128,6 @@ export class MenuComponent {
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,102 @@
|
||||
<ion-grid class="grid-wiz">
|
||||
<img width="60px" height="60px" src="/assets/img/icon.png" alt="StartOS" />
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<div class="center-container">
|
||||
<ng-container *ngIf="!caTrusted; else trusted">
|
||||
<ion-card id="untrusted" class="text-center">
|
||||
<ion-icon name="lock-closed-outline" class="wiz-icon"></ion-icon>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<h2><b>Trust your Root Certificate Authority (CA)</b></h2>
|
||||
<h1>Trust Your Root CA</h1>
|
||||
<p>
|
||||
Download and trust your server's Root CA to establish secure, encrypted
|
||||
(
|
||||
<b>HTTPS</b>
|
||||
) connections with your server
|
||||
Download and trust your server's Root Certificate Authority to establish
|
||||
a secure (HTTPS) connection. You will need to repeat this on every
|
||||
device you use to connect to your server.
|
||||
</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card">
|
||||
<ion-row class="ion-justify-content-between">
|
||||
<b class="wiz-step">1</b>
|
||||
<tui-tooltip
|
||||
content="Your server uses its Root CA to generate SSL/TLS certificates for itself and its installed services. These certificates are used to encrypt network traffic with your client devices."
|
||||
direction="right"
|
||||
></tui-tooltip>
|
||||
</ion-row>
|
||||
<div class="ion-text-center">
|
||||
<h2>Download Root CA</h2>
|
||||
<p>Download your server's Root CA</p>
|
||||
</div>
|
||||
<ion-button class="wiz-card-button" shape="round" (click)="download()">
|
||||
<ion-icon slot="start" name="download-outline"></ion-icon>
|
||||
Download
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card" [class.disabled]="!downloadClicked">
|
||||
<ion-row class="ion-justify-content-between">
|
||||
<b class="wiz-step">2</b>
|
||||
<tui-tooltip
|
||||
content="By trusting your server's Root CA, your device can verify the authenticity of its encrypted communications with your server and installed services. You will need to trust the Root CA on every device used to connect to your server."
|
||||
direction="right"
|
||||
></tui-tooltip>
|
||||
</ion-row>
|
||||
<div class="ion-text-center">
|
||||
<h2>Trust Root CA</h2>
|
||||
<p>Follow instructions for your OS</p>
|
||||
</div>
|
||||
<ion-button
|
||||
class="wiz-card-button"
|
||||
shape="round"
|
||||
(click)="instructions()"
|
||||
[disabled]="!downloadClicked"
|
||||
>
|
||||
View Docs
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card" [class.disabled]="!polling && !caTrusted">
|
||||
<b class="wiz-step">3</b>
|
||||
<div class="ion-text-center">
|
||||
<h2>Go To Login</h2>
|
||||
<p *ngIf="instructionsClicked; else space" class="inline-center">
|
||||
<ion-spinner
|
||||
class="wiz-spinner"
|
||||
*ngIf="!caTrusted; else trusted"
|
||||
></ion-spinner>
|
||||
<ng-template #trusted>
|
||||
<ion-icon name="ribbon-outline" color="success"></ion-icon>
|
||||
</ng-template>
|
||||
{{ caTrusted ? 'Root CA trusted!' : 'Waiting for trust...' }}
|
||||
</p>
|
||||
<ng-template #space>
|
||||
<!-- to keep alignment -->
|
||||
<p><br /></p>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ion-button
|
||||
class="wiz-card-button"
|
||||
shape="round"
|
||||
(click)="launchHttps()"
|
||||
[disabled]="!caTrusted"
|
||||
>
|
||||
Open
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<ol>
|
||||
<li>
|
||||
<b>Bookmark this page</b>
|
||||
- Save this page so you can access it later. You can also find the
|
||||
address in the
|
||||
<code>StartOS-info.html</code>
|
||||
file downloaded at the end of initial setup.
|
||||
</li>
|
||||
<li>
|
||||
<b>Download your server's Root CA</b>
|
||||
- Your server uses its Root CA to generate SSL/TLS certificates for
|
||||
itself and installed services. These certificates are then used to
|
||||
encrypt network traffic with your client devices.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
color="tertiary"
|
||||
(click)="download()"
|
||||
>
|
||||
Download
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
<li>
|
||||
<b>Trust your server's Root CA</b>
|
||||
- Follow instructions for your OS. By trusting your server's Root CA,
|
||||
your device can verify the authenticity of encrypted communications
|
||||
with your server.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
color="primary"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/trust-ca#establishing-trust"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
View Instructions
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
<li>
|
||||
<b>Test</b>
|
||||
- Refresh the page. If refreshing the page does not work, you may need
|
||||
to quit and re-open your browser, then revisit this page.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
class="refresh"
|
||||
(click)="refresh()"
|
||||
>
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
</ol>
|
||||
<ion-button fill="clear" (click)="launchHttps()" [disabled]="caTrusted">
|
||||
Skip
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<span class="skip_detail">(not recommended)</span>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #trusted>
|
||||
<ion-card id="trusted" class="text-center">
|
||||
<ion-icon
|
||||
name="shield-checkmark-outline"
|
||||
class="wiz-icon"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<h1>Root CA Trusted!</h1>
|
||||
<p>
|
||||
You have successfully trusted your server's Root CA and may now log in
|
||||
securely.
|
||||
</p>
|
||||
<ion-button strong (click)="launchHttps()" color="tertiary" shape="round">
|
||||
Go to login
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/eos/local.crt"
|
||||
|
||||
@@ -1,44 +1,83 @@
|
||||
.grid-wiz {
|
||||
--ion-grid-padding: 36px;
|
||||
height: 100%
|
||||
#trusted {
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.wiz-icon {
|
||||
font-size: 84px;
|
||||
#untrusted {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.wiz-card {
|
||||
.center-container {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
color: var(--ion-color-dark);
|
||||
background: #414141;
|
||||
margin: 24px;
|
||||
padding: 16px;
|
||||
height: 280px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
box-shadow: 0 4px 4px rgba(17, 17, 17, 0.144);
|
||||
border-radius: 35px;
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
|
||||
& h2 {
|
||||
font-weight: 600;
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 21px;
|
||||
line-height: 25px;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wiz-card-button {
|
||||
justify-self: center;
|
||||
white-space: normal;
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wiz-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
ol {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
text-align: left;
|
||||
|
||||
li {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
filter: saturate(0.2) contrast(0.5)
|
||||
.refresh {
|
||||
--background: var(--ion-color-success-shade);
|
||||
}
|
||||
|
||||
.wiz-step {
|
||||
margin-top: 4px;
|
||||
.wiz-icon {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.inline-center {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.skip_detail {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
margin-top: -13px;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
#trusted, #untrusted {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 701px) and (max-width: 1200px) {
|
||||
#trusted, #untrusted {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { pauseFor, RELATIVE_URL } from '@start9labs/shared'
|
||||
import { RELATIVE_URL } from '@start9labs/shared'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
@@ -11,9 +11,6 @@ import { WINDOW } from '@ng-web-apis/common'
|
||||
styleUrls: ['./ca-wizard.component.scss'],
|
||||
})
|
||||
export class CAWizardComponent {
|
||||
downloadClicked = false
|
||||
instructionsClicked = false
|
||||
polling = false
|
||||
caTrusted = false
|
||||
|
||||
constructor(
|
||||
@@ -25,51 +22,27 @@ export class CAWizardComponent {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.config.isSecure()) {
|
||||
await this.testHttps().catch(e =>
|
||||
console.warn('Failed Https connection attempt'),
|
||||
)
|
||||
}
|
||||
await this.testHttps().catch(e =>
|
||||
console.warn('Failed Https connection attempt'),
|
||||
)
|
||||
}
|
||||
|
||||
download() {
|
||||
this.downloadClicked = true
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
instructions() {
|
||||
this.windowRef.open(
|
||||
'https://docs.start9.com/0.3.5.x/getting-started/trust-ca/#trust-root-ca',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
this.instructionsClicked = true
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
private async startDaemon(): Promise<void> {
|
||||
this.polling = true
|
||||
while (this.polling) {
|
||||
try {
|
||||
await this.testHttps()
|
||||
this.polling = false
|
||||
} catch (e) {
|
||||
console.warn('Failed Https connection attempt')
|
||||
await pauseFor(2000)
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
this.document.location.reload()
|
||||
}
|
||||
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
|
||||
this.windowRef.open(`https://${host}`, '_self')
|
||||
}
|
||||
|
||||
private async testHttps() {
|
||||
const url = `https://${this.document.location.host}${this.relativeUrl}`
|
||||
await this.api.echo({ message: 'ping' }, url).then(() => {
|
||||
this.downloadClicked = true
|
||||
this.instructionsClicked = true
|
||||
this.caTrusted = true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,17 @@
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
<p style="font-weight: 600">Tor is faster over https.</p>
|
||||
<p style="font-weight: 600">
|
||||
Tor is faster over https. Your Root CA must be trusted.
|
||||
<a
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/trust-ca"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
style="color: black"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" color="light" (click)="launchHttps()">
|
||||
Open Https
|
||||
@@ -48,7 +58,6 @@
|
||||
[type]="unmasked ? 'text' : 'password'"
|
||||
[(ngModel)]="password"
|
||||
(ionChange)="error = ''"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button
|
||||
slot="end"
|
||||
|
||||
@@ -29,7 +29,7 @@ export class LoginPage {
|
||||
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
|
||||
this.windowRef.open(`https://${host}`, '_self')
|
||||
}
|
||||
|
||||
async submit() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.metric-note {
|
||||
ion-note {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export class AppShowStatusComponent {
|
||||
PrimaryStatus.Running,
|
||||
PrimaryStatus.Starting,
|
||||
PrimaryStatus.Restarting,
|
||||
].includes(this.status.primary)
|
||||
].includes(this.status.primary as PrimaryStatus)
|
||||
}
|
||||
|
||||
get isStopped(): boolean {
|
||||
|
||||
@@ -6,18 +6,25 @@
|
||||
[style.font-style]="style"
|
||||
[style.font-weight]="weight"
|
||||
>
|
||||
<span *ngIf="!installProgress">
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
|
||||
<span
|
||||
*ngIf="
|
||||
rendering.display === PR[PS.Stopping].display &&
|
||||
(sigtermTimeout | durationToSeconds) > 30
|
||||
"
|
||||
>
|
||||
this may take a while
|
||||
</span>
|
||||
|
||||
<span *ngIf="installProgress">
|
||||
<ion-text
|
||||
*ngIf="installProgress | installProgressDisplay as progress"
|
||||
color="primary"
|
||||
>
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ progress }}
|
||||
</ion-text>
|
||||
</span>
|
||||
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h2>
|
||||
For a secure local connection and faster Tor experience,
|
||||
<a
|
||||
href="https://docs.start9.com/0.3.5.x/getting-started/connecting-lan"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<logs
|
||||
[fetchLogs]="fetchLogs()"
|
||||
[followLogs]="followLogs()"
|
||||
context="eos"
|
||||
context="start-os"
|
||||
defaultBack="system"
|
||||
pageTitle="OS Logs"
|
||||
class="ion-page"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Metrics } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TimeInfo, TimeService } from 'src/app/services/time-service'
|
||||
import { TimeService } from 'src/app/services/time-service'
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -29,9 +29,24 @@ export class ServerMetricsPage {
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {}
|
||||
|
||||
private getServerData$(): Observable<[TimeInfo, Metrics]> {
|
||||
private getServerData$(): Observable<
|
||||
[
|
||||
{
|
||||
value: number
|
||||
synced: boolean
|
||||
},
|
||||
{
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
},
|
||||
Metrics,
|
||||
]
|
||||
> {
|
||||
return combineLatest([
|
||||
this.timeService.getTimeInfo$(),
|
||||
this.timeService.now$,
|
||||
this.timeService.uptime$,
|
||||
this.getMetrics$(),
|
||||
]).pipe(
|
||||
catchError(() => {
|
||||
|
||||
@@ -15,7 +15,32 @@
|
||||
|
||||
<!-- loaded -->
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<ion-item *ngIf="isTorHttp" color="warning">
|
||||
<ion-item
|
||||
*ngIf="!server['ntp-synced']"
|
||||
color="warning"
|
||||
class="ion-margin-bottom"
|
||||
>
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Clock sync failure</h2>
|
||||
<p style="font-weight: 600">
|
||||
This will cause connectivity issues. Refer to the StartOS docs to
|
||||
resolve the issue.
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
slot="end"
|
||||
color="light"
|
||||
href="https://docs.start9.com/0.3.5.x/support/common-issues#clock-sync-failure"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open Docs
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="isTorHttp" color="warning" class="ion-margin-bottom">
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
|
||||
@@ -27,7 +27,7 @@ import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
|
||||
@@ -61,7 +61,7 @@ export class ServerShowPage {
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
addClick(title: string) {
|
||||
@@ -316,7 +316,7 @@ export class ServerShowPage {
|
||||
|
||||
async launchHttps() {
|
||||
const { 'tor-address': torAddress } = await getServerInfo(this.patch)
|
||||
window.open(torAddress)
|
||||
this.windowRef.open(torAddress, '_self')
|
||||
}
|
||||
|
||||
private async setName(value: string | null): Promise<void> {
|
||||
@@ -348,7 +348,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -366,10 +365,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.shutdownServer({})
|
||||
this.presentAlertInProgress(
|
||||
action,
|
||||
'.<br /><br /><b>You will need to physically power cycle the device to regain connectivity.</b>',
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -387,7 +382,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.systemRebuild({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -433,21 +427,6 @@ export class ServerShowPage {
|
||||
alert.present()
|
||||
}
|
||||
|
||||
private async presentAlertInProgress(verb: string, message: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: `${verb} In Progress...`,
|
||||
message: `Stopping all services gracefully. This can take a while.<br /><br />If you have a speaker, your server will <b>♫ play a melody ♫</b> before shutting down. Your server will then become unreachable${message}`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'OK',
|
||||
role: 'cancel',
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
alert.present()
|
||||
}
|
||||
|
||||
settings: ServerSettings = {
|
||||
Manage: [
|
||||
{
|
||||
@@ -471,7 +450,7 @@ export class ServerShowPage {
|
||||
},
|
||||
{
|
||||
title: 'Root CA',
|
||||
description: `Download and trust your server's root certificate authority`,
|
||||
description: `Download and trust your server's Root Certificate Authority`,
|
||||
icon: 'ribbon-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['root-ca'], { relativeTo: this.route }),
|
||||
@@ -606,7 +585,7 @@ export class ServerShowPage {
|
||||
description: 'Discover what StartOS can do',
|
||||
icon: 'map-outline',
|
||||
action: () =>
|
||||
window.open(
|
||||
this.windowRef.open(
|
||||
'https://docs.start9.com/0.3.5.x/user-manual',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
@@ -619,7 +598,11 @@ export class ServerShowPage {
|
||||
description: 'Get help from the Start9 team and community',
|
||||
icon: 'chatbubbles-outline',
|
||||
action: () =>
|
||||
window.open('https://start9.com/contact', '_blank', 'noreferrer'),
|
||||
this.windowRef.open(
|
||||
'https://start9.com/contact',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
@@ -628,7 +611,7 @@ export class ServerShowPage {
|
||||
description: `Support StartOS development`,
|
||||
icon: 'logo-bitcoin',
|
||||
action: () =>
|
||||
this.document.defaultView?.open(
|
||||
this.windowRef.open(
|
||||
'https://donate.start9.com',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
|
||||
@@ -19,33 +19,35 @@ ion-card {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 0.6rem;
|
||||
font-weight: 600;
|
||||
font-size: calc(12px + 0.5vw);
|
||||
height: 3rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
min-height: 9rem;
|
||||
min-height: 8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: calc(90px + 0.5vw);
|
||||
font-size: calc(90px + 0.4vw);
|
||||
--ionicon-stroke-width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
padding: 1rem;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 0 1rem;
|
||||
font-family: 'Open Sans';
|
||||
font-size: clamp(1rem, calc(12px + 0.5vw), 1.3rem);
|
||||
height: 9rem;
|
||||
height: 4.5rem;
|
||||
width: clamp(13rem, 80%, 18rem);
|
||||
margin: 0 auto;
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-md::before {
|
||||
@@ -54,10 +56,6 @@ ion-card {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
ion-card-title,
|
||||
ion-footer {
|
||||
height: auto !important;
|
||||
}
|
||||
ion-footer {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<div #gridContent>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-justify-content-center ion-align-items-center">
|
||||
<ion-col
|
||||
*ngFor="let card of cards"
|
||||
responsiveCol
|
||||
sizeLg="4"
|
||||
sizeSm="6"
|
||||
sizeXs="12"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
<ion-col *ngFor="let card of cards" sizeXs="12">
|
||||
<widget-card
|
||||
[cardDetails]="card"
|
||||
[containerDimensions]="containerDimensions"
|
||||
|
||||
@@ -3,11 +3,17 @@ ion-col {
|
||||
--ion-grid-column-padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
@media (min-width: 1700px) {
|
||||
div {
|
||||
padding: 0 20%;
|
||||
padding: 0 7%;
|
||||
}
|
||||
ion-col {
|
||||
max-width: 24rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
div {
|
||||
padding: 0 12%;
|
||||
}
|
||||
}
|
||||
@@ -38,33 +38,33 @@ export class WidgetListComponent {
|
||||
|
||||
cards: Card[] = [
|
||||
{
|
||||
title: 'Visit the Marketplace',
|
||||
title: 'Server Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View information about your server',
|
||||
link: '/system/specs',
|
||||
},
|
||||
{
|
||||
title: 'Browse',
|
||||
icon: 'storefront-outline',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Shop for your favorite open source services',
|
||||
color: 'var(--alt-purple)',
|
||||
description: 'Browse for services to install',
|
||||
link: '/marketplace',
|
||||
qp: { back: 'true' },
|
||||
},
|
||||
{
|
||||
title: 'Root CA',
|
||||
icon: 'ribbon-outline',
|
||||
color: 'var(--alt-orange)',
|
||||
description: `Download and trust your server's root certificate authority`,
|
||||
link: '/system/root-ca',
|
||||
},
|
||||
{
|
||||
title: 'Create Backup',
|
||||
icon: 'duplicate-outline',
|
||||
color: 'var(--alt-purple)',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Back up StartOS and service data',
|
||||
link: '/system/backup',
|
||||
},
|
||||
{
|
||||
title: 'Server Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View basic information about your server',
|
||||
link: '/system/specs',
|
||||
title: 'Monitor',
|
||||
icon: 'pulse-outline',
|
||||
color: 'var(--alt-orange)',
|
||||
description: `View your system resource usage`,
|
||||
link: '/system/metrics',
|
||||
},
|
||||
{
|
||||
title: 'User Manual',
|
||||
@@ -77,7 +77,7 @@ export class WidgetListComponent {
|
||||
title: 'Contact Support',
|
||||
icon: 'chatbubbles-outline',
|
||||
color: 'var(--alt-red)',
|
||||
description: 'Get help from the Start9 team and community',
|
||||
description: 'Get help from the Start9 community',
|
||||
link: 'https://start9.com/contact',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -31,6 +31,7 @@ export module Mock {
|
||||
'current-backup': null,
|
||||
'update-progress': null,
|
||||
updated: true,
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
}
|
||||
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
||||
@@ -381,29 +382,80 @@ export module Mock {
|
||||
export function getMetrics(): Metrics {
|
||||
return {
|
||||
general: {
|
||||
temperature: (Math.random() * 100).toFixed(1),
|
||||
temperature: {
|
||||
value: '66.8',
|
||||
unit: '°C',
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
'percentage-used': '20',
|
||||
total: (Math.random() * 100).toFixed(2),
|
||||
available: '18000',
|
||||
used: '4000',
|
||||
'swap-total': '1000',
|
||||
'swap-free': Math.random().toFixed(2),
|
||||
'swap-used': '0',
|
||||
'percentage-used': {
|
||||
value: '30.7',
|
||||
unit: '%',
|
||||
},
|
||||
total: {
|
||||
value: '31971.10',
|
||||
unit: 'MiB',
|
||||
},
|
||||
available: {
|
||||
value: '22150.66',
|
||||
unit: 'MiB',
|
||||
},
|
||||
used: {
|
||||
value: '8784.97',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-total': {
|
||||
value: '7992.00',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-available': {
|
||||
value: '7882.50',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-used': {
|
||||
value: '109.50',
|
||||
unit: 'MiB',
|
||||
},
|
||||
},
|
||||
cpu: {
|
||||
'user-space': '100',
|
||||
'kernel-space': '50',
|
||||
'io-wait': String(Math.random() * 50),
|
||||
idle: '80',
|
||||
usage: '30',
|
||||
'percentage-used': {
|
||||
value: '8.4',
|
||||
unit: '%',
|
||||
},
|
||||
'user-space': {
|
||||
value: '7.0',
|
||||
unit: '%',
|
||||
},
|
||||
'kernel-space': {
|
||||
value: '1.4',
|
||||
unit: '%',
|
||||
},
|
||||
wait: {
|
||||
value: '0.5',
|
||||
unit: '%',
|
||||
},
|
||||
idle: {
|
||||
value: '91.1',
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
disk: {
|
||||
size: '1000',
|
||||
used: '900',
|
||||
available: '100',
|
||||
'percentage-used': '90',
|
||||
capacity: {
|
||||
value: '1851.60',
|
||||
unit: 'GB',
|
||||
},
|
||||
used: {
|
||||
value: '859.02',
|
||||
unit: 'GB',
|
||||
},
|
||||
available: {
|
||||
value: '992.59',
|
||||
unit: 'GB',
|
||||
},
|
||||
'percentage-used': {
|
||||
value: '46.4',
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ export module RR {
|
||||
export type EchoRes = string
|
||||
|
||||
export type GetSystemTimeReq = {} // server.time
|
||||
export type GetSystemTimeRes = string
|
||||
export type GetSystemTimeRes = {
|
||||
now: string
|
||||
uptime: number // seconds
|
||||
}
|
||||
|
||||
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsRes = LogsRes
|
||||
@@ -349,31 +352,36 @@ export interface ActionResponse {
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
interface MetricData {
|
||||
value: string
|
||||
unit: string
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
general: {
|
||||
temperature: string
|
||||
temperature: MetricData | null
|
||||
}
|
||||
memory: {
|
||||
'percentage-used': string
|
||||
total: string
|
||||
available: string
|
||||
used: string
|
||||
'swap-total': string
|
||||
'swap-free': string
|
||||
'swap-used': string
|
||||
total: MetricData
|
||||
'percentage-used': MetricData
|
||||
used: MetricData
|
||||
available: MetricData
|
||||
'zram-total': MetricData
|
||||
'zram-used': MetricData
|
||||
'zram-available': MetricData
|
||||
}
|
||||
cpu: {
|
||||
'user-space': string
|
||||
'kernel-space': string
|
||||
'io-wait': string
|
||||
idle: string
|
||||
usage: string
|
||||
'percentage-used': MetricData
|
||||
idle: MetricData
|
||||
'user-space': MetricData
|
||||
'kernel-space': MetricData
|
||||
wait: MetricData
|
||||
}
|
||||
disk: {
|
||||
size: string
|
||||
used: string
|
||||
available: string
|
||||
'percentage-used': string
|
||||
capacity: MetricData
|
||||
'percentage-used': MetricData
|
||||
used: MetricData
|
||||
available: MetricData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,10 @@ export class MockApiService extends ApiService {
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes> {
|
||||
await pauseFor(2000)
|
||||
return new Date().toUTCString()
|
||||
return {
|
||||
now: new Date().toUTCString(),
|
||||
uptime: 1234567,
|
||||
}
|
||||
}
|
||||
|
||||
async getServerLogs(
|
||||
@@ -310,6 +313,27 @@ export class MockApiService extends ApiService {
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/restarting',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/restarting',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -317,14 +341,34 @@ export class MockApiService extends ApiService {
|
||||
params: RR.ShutdownServerReq,
|
||||
): Promise<RR.ShutdownServerRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/shutting-down',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/shutting-down',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes> {
|
||||
return this.restartServer(params)
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
@@ -431,7 +475,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.enable,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> {
|
||||
@@ -472,8 +518,9 @@ export class MockApiService extends ApiService {
|
||||
value: params,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return this.withRevision(patch)
|
||||
return null
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
@@ -58,12 +58,13 @@ export const mockPatchData: DataModel = {
|
||||
'current-backup': null,
|
||||
updated: false,
|
||||
'update-progress': null,
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
},
|
||||
hostname: 'random-words',
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
|
||||
'system-start-time': new Date(new Date().valueOf() - 360042).toUTCString(),
|
||||
'ntp-synced': false,
|
||||
zram: false,
|
||||
smtp: {
|
||||
server: '',
|
||||
@@ -75,6 +76,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
platform: 'x86_64-nonfree',
|
||||
},
|
||||
'package-data': {
|
||||
bitcoind: Mock.bitcoind,
|
||||
|
||||
@@ -4,8 +4,6 @@ import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
const {
|
||||
packageArch,
|
||||
osArch,
|
||||
gitHash,
|
||||
useMocks,
|
||||
ui: { api, marketplace, mocks },
|
||||
@@ -25,8 +23,6 @@ export class ConfigService {
|
||||
version = require('../../../../../package.json').version as string
|
||||
useMocks = useMocks
|
||||
mocks = mocks
|
||||
packageArch = packageArch
|
||||
osArch = osArch
|
||||
gitHash = gitHash
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
|
||||
@@ -4,12 +4,12 @@ import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
HealthResult,
|
||||
InstalledPackageDataEntry,
|
||||
InstalledPackageInfo,
|
||||
PackageMainStatus,
|
||||
} from './patch-db/data-model'
|
||||
import * as deepEqual from 'fast-deep-equal'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
|
||||
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
||||
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
||||
@@ -62,7 +62,13 @@ export class DepErrorService {
|
||||
return currentDeps(pkgs, pkgId).reduce(
|
||||
(innerErrors, depId): PkgDependencyErrors => ({
|
||||
...innerErrors,
|
||||
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
|
||||
[depId]: this.getDepError(
|
||||
pkgs,
|
||||
pkgInstalled,
|
||||
pkgs[pkgId].manifest,
|
||||
depId,
|
||||
outerErrors,
|
||||
),
|
||||
}),
|
||||
{} as PkgDependencyErrors,
|
||||
)
|
||||
@@ -70,11 +76,13 @@ export class DepErrorService {
|
||||
|
||||
private getDepError(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgInstalled: InstalledPackageDataEntry,
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
outerErrors: AllDependencyErrors,
|
||||
): DependencyError | null {
|
||||
const depInstalled = pkgs[depId]?.installed
|
||||
const depManifest = pkgs[depId]?.manifest
|
||||
|
||||
// not installed
|
||||
if (!depInstalled) {
|
||||
@@ -83,9 +91,6 @@ export class DepErrorService {
|
||||
}
|
||||
}
|
||||
|
||||
const pkgManifest = pkgInstalled.manifest
|
||||
const depManifest = depInstalled.manifest
|
||||
|
||||
// incorrect version
|
||||
if (
|
||||
!this.emver.satisfies(
|
||||
|
||||
@@ -66,10 +66,11 @@ export interface ServerInfo {
|
||||
hostname: string
|
||||
pubkey: string
|
||||
'ca-fingerprint': string
|
||||
'system-start-time': string
|
||||
'ntp-synced': boolean
|
||||
zram: boolean
|
||||
smtp: typeof customSmtp.validator._TYPE
|
||||
'password-hash': string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export interface IpInfo {
|
||||
@@ -90,9 +91,16 @@ export interface ServerStatusInfo {
|
||||
}
|
||||
updated: boolean
|
||||
'update-progress': { size: number | null; downloaded: number } | null
|
||||
restarting: boolean
|
||||
'shutting-down': boolean
|
||||
}
|
||||
|
||||
export enum ServerStatus {
|
||||
Running = 'running',
|
||||
Updated = 'updated',
|
||||
BackingUp = 'backing-up',
|
||||
}
|
||||
|
||||
export interface PackageDataEntry {
|
||||
state: PackageState
|
||||
manifest: Manifest
|
||||
|
||||
@@ -1,85 +1,59 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
combineLatest,
|
||||
from,
|
||||
Observable,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
|
||||
export interface TimeInfo {
|
||||
systemStartTime: number
|
||||
systemCurrentTime: number
|
||||
systemUptime: {
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
}
|
||||
import { combineLatest, interval, of } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TimeService {
|
||||
private readonly systemStartTime$ = this.patch
|
||||
.watch$('server-info', 'system-start-time')
|
||||
.pipe(map(startTime => new Date(startTime).valueOf()))
|
||||
private readonly time$ = of({}).pipe(
|
||||
switchMap(() => this.apiService.getSystemTime({})),
|
||||
switchMap(({ now, uptime }) => {
|
||||
const current = new Date(now).valueOf()
|
||||
return interval(1000).pipe(
|
||||
map(index => {
|
||||
const incremented = index + 1
|
||||
return {
|
||||
now: current + 1000 * incremented,
|
||||
uptime: uptime + incremented,
|
||||
}
|
||||
}),
|
||||
startWith({
|
||||
now: current,
|
||||
uptime,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly now$ = combineLatest([
|
||||
this.time$,
|
||||
this.patch.watch$('server-info', 'ntp-synced'),
|
||||
]).pipe(
|
||||
map(([time, synced]) => ({
|
||||
value: time.now,
|
||||
synced,
|
||||
})),
|
||||
)
|
||||
|
||||
readonly uptime$ = this.time$.pipe(
|
||||
map(({ uptime }) => {
|
||||
const days = Math.floor(uptime / (24 * 60 * 60))
|
||||
const daysSec = uptime % (24 * 60 * 60)
|
||||
const hours = Math.floor(daysSec / (60 * 60))
|
||||
const hoursSec = uptime % (60 * 60)
|
||||
const minutes = Math.floor(hoursSec / 60)
|
||||
const seconds = uptime % 60
|
||||
return { days, hours, minutes, seconds }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly apiService: ApiService,
|
||||
) {}
|
||||
|
||||
getTimeInfo$(): Observable<TimeInfo> {
|
||||
return combineLatest([
|
||||
this.systemStartTime$.pipe(),
|
||||
this.getSystemCurrentTime$(),
|
||||
]).pipe(
|
||||
map(([systemStartTime, systemCurrentTime]) => ({
|
||||
systemStartTime,
|
||||
systemCurrentTime,
|
||||
systemUptime: this.getSystemUptime(systemStartTime, systemCurrentTime),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
private getSystemCurrentTime$() {
|
||||
return from(this.apiService.getSystemTime({})).pipe(
|
||||
switchMap(utcStr => {
|
||||
const dateObj = new Date(utcStr)
|
||||
const current = dateObj.valueOf()
|
||||
return timer(0, 1000).pipe(
|
||||
map(index => {
|
||||
const incremented = index + 1
|
||||
const msToAdd = 1000 * incremented
|
||||
return current + msToAdd
|
||||
}),
|
||||
startWith(current),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private getSystemUptime(systemStartTime: number, systemCurrentTime: number) {
|
||||
const ms = systemCurrentTime - systemStartTime
|
||||
|
||||
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
|
||||
const daysms = ms % (24 * 60 * 60 * 1000)
|
||||
|
||||
const hours = Math.floor(daysms / (60 * 60 * 1000))
|
||||
const hoursms = ms % (60 * 60 * 1000)
|
||||
|
||||
const minutes = Math.floor(hoursms / (60 * 1000))
|
||||
const minutesms = ms % (60 * 1000)
|
||||
|
||||
const seconds = Math.floor(minutesms / 1000)
|
||||
|
||||
return { days, hours, minutes, seconds }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UiLauncherService {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
launch(addressInfo: InstalledPackageInfo['address-info']): void {
|
||||
const UIs = Object.values(addressInfo)
|
||||
.filter(info => info.ui)
|
||||
.map(info => ({
|
||||
name: info.name,
|
||||
addresses: info.addresses,
|
||||
}))
|
||||
|
||||
if (UIs.length === 1 && UIs[0].addresses.length === 1) {
|
||||
this.document.defaultView?.open(
|
||||
UIs[0].addresses[0],
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user