merge from master and fix typescript errors

This commit is contained in:
Matt Hill
2023-11-08 15:44:05 -07:00
133 changed files with 3006 additions and 19797 deletions

View File

@@ -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"

View File

@@ -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,

View File

@@ -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'

View File

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

View File

@@ -1,21 +0,0 @@
import { Directive, HostListener, Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common'
import { debounce } from '@start9labs/shared'
@Directive({
selector: '[appEnter]',
})
export class EnterDirective {
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
@HostListener('document:keydown.enter')
@debounce()
handleKeyboardEvent() {
const elems = this.document.querySelectorAll('.enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement
if (elem && !elem.classList.contains('no-click') && !elem.disabled) {
elem.click()
}
}
}

View File

@@ -1,9 +0,0 @@
import { NgModule } from '@angular/core'
import { EnterDirective } from './enter.directive'
@NgModule({
declarations: [EnterDirective],
exports: [EnterDirective],
})
export class EnterModule {}

View File

@@ -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"

View File

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

View File

@@ -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>
&nbsp;{{ 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"

View File

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

View File

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

View File

@@ -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"

View File

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

View File

@@ -1,3 +1,4 @@
.metric-note {
ion-note {
font-size: 16px;
color: white;
}

View File

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

View File

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

View File

@@ -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"
>

View File

@@ -1,7 +1,7 @@
<logs
[fetchLogs]="fetchLogs()"
[followLogs]="followLogs()"
context="eos"
context="start-os"
defaultBack="system"
pageTitle="OS Logs"
class="ion-page"

View File

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

View File

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

View File

@@ -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',

View File

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

View File

@@ -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"

View File

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

View File

@@ -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',
},
]

View File

@@ -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: '%',
},
},
}
}

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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(

View File

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

View File

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

View File

@@ -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',
)
}
}
}