address comments and more

This commit is contained in:
Matt Hill
2024-08-15 08:05:37 -06:00
parent a730543c76
commit 015131f198
24 changed files with 316 additions and 396 deletions

17
web/package-lock.json generated
View File

@@ -5254,23 +5254,6 @@
"rxjs": ">=6.0.0"
}
},
"node_modules/@taiga-ui/experimental": {
"version": "4.0.0-rc.7",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.3"
},
"peerDependencies": {
"@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0",
"@taiga-ui/addon-commerce": "^4.0.0-rc.7",
"@taiga-ui/cdk": "^4.0.0-rc.7",
"@taiga-ui/core": "^4.0.0-rc.7",
"@taiga-ui/kit": "^4.0.0-rc.7",
"@taiga-ui/polymorpheus": "^4.6.4",
"rxjs": ">=7.0.0"
}
},
"node_modules/@taiga-ui/i18n": {
"version": "4.0.0-rc.7",
"license": "Apache-2.0",

View File

@@ -14,7 +14,7 @@ import { PolymorpheusContent } from '@taiga-ui/polymorpheus'
@Component({
selector: 'marketplace-package-screenshots',
template: `
<!--@TODO Matt or Lucy?-->
<!--@TODO future release-->
<div
*ngIf="$any(pkg).screenshots as screenshots"
tuiCarouselButtons

View File

@@ -40,7 +40,7 @@ export default class LoadingPage {
filter(Boolean),
take(1),
switchMap(({ guid, progress }) =>
this.api.openProgressWebsocket$(guid).pipe(
this.api.openWebsocket$<T.FullProgress>(guid).pipe(
startWith(progress),
catchError((_, watch$) =>
interval(2000).pipe(

View File

@@ -2,9 +2,7 @@ import * as jose from 'node-jose'
import {
DiskInfo,
DiskListResponse,
FollowLogsReq,
FollowLogsRes,
Log,
PartitionInfo,
StartOSDiskInfo,
} from '@start9labs/shared'
@@ -25,11 +23,8 @@ export abstract class ApiService {
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
abstract complete(): Promise<T.SetupResult> // setup.complete
abstract exit(): Promise<void> // setup.exit
abstract followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> // setup.logs.follow
abstract openLogsWebsocket$(
config: WebSocketSubjectConfig<Log>,
): Observable<Log>
abstract openProgressWebsocket$(guid: string): Observable<T.FullProgress>
abstract followServerLogs(): Promise<FollowLogsRes> // setup.logs.follow
abstract openWebsocket$<T>(guid: string): Observable<T>
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
if (!this.pubkey) throw new Error('No pubkey found!')

View File

@@ -3,11 +3,9 @@ import { Inject, Injectable } from '@angular/core'
import {
DiskListResponse,
encodeBase64,
FollowLogsReq,
FollowLogsRes,
HttpService,
isRpcError,
Log,
RpcError,
RPCOptions,
StartOSDiskInfo,
@@ -15,7 +13,7 @@ import {
import { T } from '@start9labs/start-sdk'
import * as jose from 'node-jose'
import { Observable } from 'rxjs'
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
import { webSocket } from 'rxjs/webSocket'
import { ApiService } from './api.service'
@Injectable({
@@ -29,12 +27,13 @@ export class LiveApiService extends ApiService {
super()
}
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
openWebsocket$<T>(guid: string): Observable<T> {
const { location } = this.document.defaultView!
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
const host = location.host
return webSocket({
url: `ws://${host}/ws/rpc/${guid}`,
url: `${protocol}://${host}/ws/rpc/${guid}`,
})
}
@@ -99,12 +98,8 @@ export class LiveApiService extends ApiService {
})
}
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
return this.rpcRequest({ method: 'setup.logs.follow', params })
}
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
return webSocket(`http://start.local/ws/${url}`)
async followServerLogs(): Promise<FollowLogsRes> {
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
}
async complete(): Promise<T.SetupResult> {

View File

@@ -2,16 +2,13 @@ import { Injectable } from '@angular/core'
import {
DiskListResponse,
encodeBase64,
FollowLogsReq,
FollowLogsRes,
Log,
pauseFor,
StartOSDiskInfo,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import * as jose from 'node-jose'
import { interval, map, Observable, of } from 'rxjs'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { interval, map, Observable } from 'rxjs'
import { ApiService } from './api.service'
@Injectable({
@@ -47,68 +44,102 @@ export class MockApiService extends ApiService {
// websocket
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
return of(PROGRESS)
// const numPhases = PROGRESS.phases.length
// oldMockProgress$(): Promise<T.FullProgress> {
// const numPhases = PROGRESS.phases.length
// return of(PROGRESS).pipe(
// switchMap(full =>
// from(PROGRESS.phases).pipe(
// mergeScan((full, phase, i) => {
// if (
// !phase.progress ||
// typeof phase.progress !== 'object' ||
// !phase.progress.total
// ) {
// full.phases[i].progress = true
// return of(PROGRESS).pipe(
// switchMap(full =>
// from(PROGRESS.phases).pipe(
// mergeScan((full, phase, i) => {
// if (
// !phase.progress ||
// typeof phase.progress !== 'object' ||
// !phase.progress.total
// ) {
// full.phases[i].progress = true
// if (
// full.overall &&
// typeof full.overall === 'object' &&
// full.overall.total
// ) {
// const step = full.overall.total / numPhases
// full.overall.done += step
// }
// if (
// full.overall &&
// typeof full.overall === 'object' &&
// full.overall.total
// ) {
// const step = full.overall.total / numPhases
// full.overall.done += step
// }
// return of(full).pipe(delay(2000))
// } else {
// const total = phase.progress.total
// const step = total / 4
// let done = phase.progress.done
// return of(full).pipe(delay(2000))
// } else {
// const total = phase.progress.total
// const step = total / 4
// let done = phase.progress.done
// return interval(1000).pipe(
// takeWhile(() => done < total),
// map(() => {
// done += step
// return interval(1000).pipe(
// takeWhile(() => done < total),
// map(() => {
// done += step
// console.error(done)
// console.error(done)
// if (
// full.overall &&
// typeof full.overall === 'object' &&
// full.overall.total
// ) {
// const step = full.overall.total / numPhases / 4
// if (
// full.overall &&
// typeof full.overall === 'object' &&
// full.overall.total
// ) {
// const step = full.overall.total / numPhases / 4
// full.overall.done += step
// }
// full.overall.done += step
// }
// if (done === total) {
// full.phases[i].progress = true
// if (done === total) {
// full.phases[i].progress = true
// if (i === numPhases - 1) {
// full.overall = true
// }
// }
// return full
// }),
// )
// }
// }, full),
// ),
// ),
// )
// if (i === numPhases - 1) {
// full.overall = true
// }
// }
// return full
// }),
// )
// }
// }, full),
// ),
// ),
// )
// }
openWebsocket$<T>(guid: string): Observable<T> {
if (guid === 'logs-guid') {
return interval(500).pipe(
map(() => ({
timestamp: new Date().toISOString(),
message: 'fake log entry',
bootId: 'boot-id',
})),
) as Observable<T>
} else if (guid === 'progress-guid') {
// @TODO mock progress
return interval(1000).pipe(
map(() => ({
overall: true,
phases: [
{
name: 'Preparing Data',
progress: true,
},
{
name: 'Transferring Data',
progress: true,
},
{
name: 'Finalizing Setup',
progress: true,
},
],
})),
) as Observable<T>
} else {
throw new Error('invalid guid type')
}
}
private statusIndex = 0
@@ -270,24 +301,14 @@ export class MockApiService extends ApiService {
}
}
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
async followServerLogs(): Promise<FollowLogsRes> {
await pauseFor(1000)
return {
startCursor: 'fakestartcursor',
guid: 'fake-guid',
guid: 'logs-guid',
}
}
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
return interval(500).pipe(
map(() => ({
timestamp: new Date().toISOString(),
message: 'fake log entry',
bootId: 'boot-id',
})),
)
}
async complete(): Promise<T.SetupResult> {
await pauseFor(1000)
return {

View File

@@ -1,13 +1,12 @@
import { StaticClassProvider } from '@angular/core'
import { bufferTime, defer, map, Observable, scan, switchMap } from 'rxjs'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
import { Constructor } from '../types/constructor'
import { convertAnsi } from '../util/convert-ansi'
interface Api {
followServerLogs: (params: FollowLogsReq) => Promise<FollowLogsRes>
openLogsWebsocket$: (config: WebSocketSubjectConfig<Log>) => Observable<Log>
openWebsocket$: (guid: string) => Observable<Log>
}
export function provideSetupLogsService(
@@ -22,9 +21,7 @@ export function provideSetupLogsService(
export class SetupLogsService extends Observable<readonly string[]> {
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
switchMap(({ guid }) =>
this.api.openLogsWebsocket$({ url: `/rpc/${guid}` }),
),
switchMap(({ guid }) => this.api.openWebsocket$(guid)),
bufferTime(1000),
map(convertAnsi),
scan((logs: readonly string[], log) => [...logs, log], []),

View File

@@ -1,4 +1,4 @@
// @TODO Matt this is T.FullProgress but shared does not depend on sdk
// @TODO get types from sdk
type Progress = null | boolean | { done: number; total: number | null }
type NamedProgress = { name: string; progress: Progress }
type FullProgress = { overall: Progress; phases: Array<NamedProgress> }

View File

@@ -33,7 +33,7 @@ export default class InitializingPage {
private readonly state = inject(StateService)
readonly progress = toSignal(
defer(() => from(this.api.initGetProgress())).pipe(
defer(() => from(this.api.initFollowProgress())).pipe(
switchMap(({ guid, progress }) =>
this.api
.openWebsocket$<T.FullProgress>(guid, {})

View File

@@ -1,4 +1,4 @@
import { CB, CT, T } from '@start9labs/start-sdk'
import { CB, CT, T, utils } from '@start9labs/start-sdk'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit'
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
@@ -53,75 +53,44 @@ export type AddressDetails = {
url: string
}
// @TODO Matt these types have change significantly
export function getAddresses(serviceInterface: any): {
// T.ServiceInterface): {
export function getMultihostAddresses(
serviceInterface: T.ServiceInterface,
host: T.Host,
): {
clearnet: AddressDetails[]
local: AddressDetails[]
tor: AddressDetails[]
} {
const host = serviceInterface.hostInfo
const addressInfo = serviceInterface.addressInfo
const username = addressInfo.username ? addressInfo.username + '@' : ''
const suffix = addressInfo.suffix || ''
const hostnames =
host.kind === 'multi'
? host.hostnames
: host.hostname
? [host.hostname]
: []
const hostnamesInfo = host.hostnameInfo[addressInfo.internalPort]
const clearnet: AddressDetails[] = []
const local: AddressDetails[] = []
const tor: AddressDetails[] = []
hostnames.forEach((h: any) => {
let scheme = ''
let port = ''
if (h.hostname.sslPort) {
port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}`
scheme = addressInfo.bindOptions.addSsl?.scheme
? `${addressInfo.bindOptions.addSsl.scheme}://`
: ''
}
if (h.hostname.port) {
port = h.hostname.port === 80 ? '' : `:${h.hostname.port}`
scheme = addressInfo.bindOptions.scheme
? `${addressInfo.bindOptions.scheme}://`
: ''
}
if (h.kind === 'onion') {
tor.push({
label: h.hostname.sslPort ? 'HTTPS' : 'HTTP',
url: toHref(scheme, username, h.hostname.value, port, suffix),
})
} else {
const hostnameKind = h.hostname.kind
if (hostnameKind === 'domain') {
tor.push({
url: toHref(
scheme,
username,
`${h.hostname.subdomain}.${h.hostname.domain}`,
port,
suffix,
),
})
hostnamesInfo.forEach(hostnameInfo => {
utils.addressHostToUrl(addressInfo, hostnameInfo).forEach(url => {
// Onion
if (hostnameInfo.kind === 'onion') {
tor.push({ url })
// IP
} else {
local.push({
label:
hostnameKind === 'local'
? 'Local'
: `${h.networkInterfaceId} (${hostnameKind})`,
url: toHref(scheme, username, h.hostname.value, port, suffix),
})
// Domain
if (hostnameInfo.hostname.kind === 'domain') {
clearnet.push({ url })
// Local
} else {
const hostnameKind = hostnameInfo.hostname.kind
local.push({
label:
hostnameKind === 'local'
? 'Local'
: `${hostnameInfo.networkInterfaceId} (${hostnameKind})`,
url,
})
}
}
}
})
})
return {
@@ -130,13 +99,3 @@ export function getAddresses(serviceInterface: any): {
tor,
}
}
function toHref(
scheme: string,
username: string,
hostname: string,
port: string,
suffix: string,
): string {
return `${scheme}${username}${hostname}${port}${suffix}`
}

View File

@@ -1,5 +1,5 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { convertAnsi, toLocalIsoString } from '@start9labs/shared'
import { convertAnsi, Log, toLocalIsoString } from '@start9labs/shared'
import {
bufferTime,
catchError,
@@ -44,7 +44,13 @@ export class LogsPipe implements PipeTransform {
),
defer(() => followLogs(this.options)).pipe(
tap(r => this.logs.setCursor(r.startCursor)),
switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))),
switchMap(r =>
this.api.openWebsocket$<Log>(r.guid, {
openObserver: {
next: () => this.logs.status$.next('connected'),
},
}),
),
bufferTime(1000),
filter(logs => !!logs.length),
map(convertAnsi),
@@ -67,15 +73,6 @@ export class LogsPipe implements PipeTransform {
private get options() {
return this.logs.status$.value === 'connected' ? { limit: 400 } : {}
}
private toConfig(guid: string) {
return {
url: `/rpc/${guid}`,
openObserver: {
next: () => this.logs.status$.next('connected'),
},
}
}
}
function getMessage(success: boolean): string {

View File

@@ -3,17 +3,17 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
import { combineLatest, map } from 'rxjs'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getAddresses } from '../../../components/interfaces/interface.utils'
import { getMultihostAddresses } from '../../../components/interfaces/interface.utils'
@Component({
template: `
<app-interface
*ngIf="interfaceInfo$ | async as interfaceInfo"
*ngIf="interfacesWithAddresses$ | async as serviceInterface"
[packageContext]="context"
[serviceInterface]="interfaceInfo"
[serviceInterface]="serviceInterface"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -22,23 +22,25 @@ import { getAddresses } from '../../../components/interfaces/interface.utils'
})
export class ServiceInterfaceRoute {
private readonly route = inject(ActivatedRoute)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly context = {
packageId: getPkgId(this.route),
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
}
readonly interfaceInfo$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$(
readonly interfacesWithAddresses$ = combineLatest([
this.patch.watch$(
'packageData',
this.context.packageId,
'serviceInterfaces',
this.context.interfaceId,
)
.pipe(
map(info => ({
...info,
addresses: getAddresses(info),
})),
)
),
this.patch.watch$('packageData', this.context.packageId, 'hosts'),
]).pipe(
map(([iFace, hosts]) => ({
...iFace,
addresses: getMultihostAddresses(iFace, hosts[iFace.addressInfo.hostId]),
})),
)
}

View File

@@ -23,6 +23,7 @@ import { BackupType } from '../types/backup-type'
export class BackupsStatusComponent {
private readonly exver = inject(Exver)
@Input({ required: true }) serverId!: string
@Input({ required: true }) type!: BackupType
@Input({ required: true }) target!: BackupTarget
@@ -61,8 +62,12 @@ export class BackupsStatusComponent {
}
private get hasBackup(): boolean {
return !!this.target.startOs
// @TODO Matt types changed
// && this.exver.compareExver(this.target.startOs.version, '0.3.0') !== -1
return (
this.target.startOs[this.serverId] &&
this.exver.compareOsVersion(
this.target.startOs[this.serverId].version,
'0.3.6',
) !== 'less'
)
}
}

View File

@@ -23,6 +23,9 @@ import { BackupsStatusComponent } from '../components/status.component'
import { GetDisplayInfoPipe } from '../pipes/get-display-info.pipe'
import { BackupType } from '../types/backup-type'
import { TARGETS } from './targets.component'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
template: `
@@ -40,7 +43,12 @@ import { TARGETS } from './targets.component'
<tui-icon [icon]="displayInfo.icon" />
<div>
<strong>{{ displayInfo.name }}</strong>
<backups-status [type]="context.data.type" [target]="target" />
<backups-status
[type]="context.data.type"
[target]="target"
[serverId]="serverId"
]
/>
<div [style.color]="'var(--tui-text-secondary'">
{{ displayInfo.description }}
<br />
@@ -69,6 +77,7 @@ export class BackupsTargetModal {
private readonly dialogs = inject(TuiDialogService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly patch = inject(PatchDB<DataModel>)
readonly context =
inject<TuiDialogContext<BackupTarget, { type: BackupType }>>(
@@ -81,10 +90,12 @@ export class BackupsTargetModal {
? 'Loading Backup Targets'
: 'Loading Backup Sources'
serverId = ''
targets: BackupTarget[] = []
async ngOnInit() {
try {
this.serverId = (await getServerInfo(this.patch)).id
this.targets = (await this.api.getBackupTargets({})).saved
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -1,18 +1,32 @@
import { inject, Injectable } from '@angular/core'
import { Observable, retry, shareReplay } from 'rxjs'
import { Metrics } from 'src/app/services/api/api.types'
import {
defer,
Observable,
retry,
shareReplay,
startWith,
switchMap,
} from 'rxjs'
import { ServerMetrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@Injectable({
providedIn: 'root',
})
export class MetricsService extends Observable<Metrics> {
// @TODO get real url, 'rpc/{guid}' or something like that
private readonly metrics$ = inject(ApiService)
.openMetricsWebsocket$({
url: '',
})
.pipe(retry(), shareReplay(1))
export class MetricsService extends Observable<ServerMetrics> {
private readonly api = inject(ApiService)
// @TODO Alex do we need to use defer? I am unsure when this is necessary.
private readonly metrics$ = defer(() =>
this.api.followServerMetrics({}),
).pipe(
switchMap(({ guid, metrics }) =>
this.api.openWebsocket$<ServerMetrics>(guid).pipe(startWith(metrics)),
),
// @TODO Alex how to handle failure and reconnection here? Simple retry() will not work. Seems like we need a general solution for reconnecting websockets: patchDB, logs, metrics, progress, and any future. Reconnection should depend on server state, then we need to get a new guid, then reconnect. Similar to how patchDB websocket currently behaves on disconnect/reconnect.
retry(),
shareReplay(),
)
constructor() {
super(subscriber => this.metrics$.subscribe(subscriber))

View File

@@ -7,7 +7,7 @@ import {
InterfaceComponent,
ServiceInterfaceWithAddresses,
} from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { getMultihostAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
@@ -29,7 +29,6 @@ export class StartOsUiComponent {
.watch$('serverInfo', 'ui')
.pipe(
map(hosts => {
// @TODO Matt fix types
const serviceInterface: T.ServiceInterface = {
id: 'startos-ui',
name: 'StartOS UI',
@@ -41,31 +40,26 @@ export class StartOsUiComponent {
addressInfo: {
hostId: '',
username: null,
internalPort: 80,
scheme: 'http',
sslScheme: 'https',
suffix: '',
bindOptions: {
scheme: 'http',
preferredExternalPort: 80,
addSsl: {
scheme: 'https',
preferredExternalPort: 443,
// @TODO is this alpn correct?
alpn: { specified: ['http/1.1', 'h2'] },
},
secure: {
ssl: false,
},
},
},
hostInfo: {
id: 'start-os-ui-host',
kind: 'multi',
hostnames: hosts,
}
// @TODO Aiden confirm this is correct
const host: T.Host = {
kind: 'multi',
bindings: {},
hostnameInfo: {
80: hosts,
},
} as any
addresses: [],
}
return {
...serviceInterface,
addresses: getAddresses(serviceInterface),
addresses: getMultihostAddresses(serviceInterface, host),
}
}),
)

View File

@@ -3,12 +3,7 @@ import {
PackageDataEntry,
ServerStatusInfo,
} from 'src/app/services/patch-db/data-model'
import {
Metrics,
NotificationLevel,
RR,
ServerNotifications,
} from './api.types'
import { RR, ServerMetrics, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
@@ -748,7 +743,7 @@ export module Mock {
packageId: null,
createdAt: '2019-12-26T14:20:30.872Z',
code: 1,
level: NotificationLevel.Success,
level: 'success',
title: 'Backup Complete',
message: 'StartOS and services have been successfully backed up.',
data: {
@@ -769,7 +764,7 @@ export module Mock {
packageId: null,
createdAt: '2019-12-26T14:20:30.872Z',
code: 2,
level: NotificationLevel.Warning,
level: 'warning',
title: 'SSH Key Added',
message: 'A new SSH key was added. If you did not do this, shit is bad.',
data: null,
@@ -780,7 +775,7 @@ export module Mock {
packageId: null,
createdAt: '2019-12-26T14:20:30.872Z',
code: 3,
level: NotificationLevel.Info,
level: 'info',
title: 'SSH Key Removed',
message: 'A SSH key was removed.',
data: null,
@@ -791,7 +786,7 @@ export module Mock {
packageId: 'bitcoind',
createdAt: '2019-12-26T14:20:30.872Z',
code: 4,
level: NotificationLevel.Error,
level: 'error',
title: 'Service Crashed',
message: new Array(3)
.fill(
@@ -806,7 +801,7 @@ export module Mock {
},
]
export function getMetrics(): Metrics {
export function getMetrics(): ServerMetrics {
return {
general: {
temperature: {
@@ -1020,8 +1015,16 @@ export module Mock {
path: '/Desktop/embassy-backups',
username: 'TestUser',
mountable: false,
// @TODO Matt Provide mock for startOs
startOs: {},
startOs: {
abcdefgh: {
hostname: 'adjective-noun.local',
version: '0.3.6',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: '',
},
},
},
{
id: 'ftcvewdnkemfksdm',
@@ -1054,8 +1057,16 @@ export module Mock {
vendor: 'SSK',
mountable: true,
path: '/HomeFolder/Documents',
// @TODO Matt Provide mock for startOs
startOs: {},
startOs: {
'different-server': {
hostname: 'different-server.local',
version: '0.3.6',
timestamp: new Date().toISOString(),
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
wrappedKey: '',
},
},
},
],
}

View File

@@ -61,7 +61,7 @@ export module RR {
// init
export type InitGetProgressRes = {
export type InitFollowProgressRes = {
progress: T.FullProgress
guid: string
}
@@ -88,10 +88,10 @@ export module RR {
guid: string
}
export type GetServerMetricsReq = {} // server.metrics
export type GetServerMetricsRes = {
export type FollowServerMetricsReq = {} // server.metrics.follow
export type FollowServerMetricsRes = {
guid: string
metrics: Metrics
metrics: ServerMetrics
}
export type UpdateServerReq = { registry: string } // server.update
@@ -339,9 +339,11 @@ export module RR {
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
export type FollowPackageLogsRes = FollowServerLogsRes
export type GetPackageMetricsReq = { id: string } // package.metrics
// @TODO Matt create package metrics type
export type GetPackageMetricsRes = any
export type FollowPackageMetricsReq = { id: string } // package.metrics.follow
export type FollowPackageMetricsRes = {
guid: string
metrics: AppMetrics
}
export type InstallPackageReq = T.InstallParams
export type InstallPackageRes = null
@@ -415,25 +417,6 @@ export module RR {
} // package.proxy.set-outbound
export type SetServiceOutboundProxyRes = null
// marketplace
export type GetMarketplaceInfoReq = { serverId: string }
// @TODO Matt fix type
export type GetMarketplaceInfoRes = any
export type GetMarketplaceEosReq = { serverId: string }
// @TODO Matt fix type
export type GetMarketplaceEosRes = any
export type GetMarketplacePackagesReq = {
ids?: { id: string; version: string }[]
// iff !ids
category?: string
query?: string
page?: number
perPage?: number
}
// registry
/** these are returned in ASCENDING order. the newest available version will be the LAST in the object */
@@ -443,34 +426,34 @@ export module RR {
export type CheckOSUpdateRes = OSUpdate
}
export interface OSUpdate {
export type OSUpdate = {
version: string
headline: string
releaseNotes: { [version: string]: string }
}
export interface Breakages {
export type Breakages = {
[id: string]: TaggedDependencyError
}
export interface TaggedDependencyError {
export type TaggedDependencyError = {
dependency: string
error: DependencyError
}
export interface ActionResponse {
export type ActionResponse = {
message: string
value: string | null
copyable: boolean
qr: boolean
}
interface MetricData {
type MetricData = {
value: string
unit: string
}
export interface Metrics {
export type ServerMetrics = {
general: {
temperature: MetricData | null
}
@@ -498,14 +481,28 @@ export interface Metrics {
}
}
export interface Session {
export type AppMetrics = {
memory: {
percentageUsed: MetricData
used: MetricData
}
cpu: {
percentageUsed: MetricData
}
disk: {
percentageUsed: MetricData
used: MetricData
}
}
export type Session = {
loggedIn: string
lastActive: string
userAgent: string
metadata: SessionMetadata
}
export interface SessionMetadata {
export type SessionMetadata = {
platforms: PlatformType[]
}
@@ -565,7 +562,7 @@ export interface CloudBackupTarget extends BaseBackupTarget {
provider: 'dropbox' | 'google-drive'
}
export interface BackupRun {
export type BackupRun = {
id: string
startedAt: string
completedAt: string
@@ -574,7 +571,7 @@ export interface BackupRun {
report: BackupReport
}
export interface BackupJob {
export type BackupJob = {
id: string
name: string
target: BackupTarget
@@ -582,7 +579,7 @@ export interface BackupJob {
packageIds: string[]
}
export interface BackupInfo {
export type BackupInfo = {
version: string
timestamp: string
packageBackups: {
@@ -590,18 +587,18 @@ export interface BackupInfo {
}
}
export interface PackageBackupInfo {
export type PackageBackupInfo = {
title: string
version: string
osVersion: string
timestamp: string
}
export interface ServerSpecs {
export type ServerSpecs = {
[key: string]: string | number
}
export interface SSHKey {
export type SSHKey = {
createdAt: string
alg: string
hostname: string
@@ -610,32 +607,25 @@ export interface SSHKey {
export type ServerNotifications = ServerNotification<number>[]
export interface ServerNotification<T extends number> {
export type ServerNotification<T extends number> = {
id: number
packageId: string | null
createdAt: string
code: T
level: NotificationLevel
level: 'success' | 'info' | 'warning' | 'error'
title: string
message: string
data: NotificationData<T>
read: boolean
}
export enum NotificationLevel {
Success = 'success',
Info = 'info',
Warning = 'warning',
Error = 'error',
}
export type NotificationData<T> = T extends 0
? null
: T extends 1
? BackupReport
: any
export interface BackupReport {
export type BackupReport = {
server: {
attempted: boolean
error: string | null
@@ -647,7 +637,7 @@ export interface BackupReport {
}
}
export interface AvailableWifi {
export type AvailableWifi = {
ssid: string
strength: number
security: string[]
@@ -682,29 +672,29 @@ export type DependencyError =
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export interface DependencyErrorNotInstalled {
export type DependencyErrorNotInstalled = {
type: 'notInstalled'
}
export interface DependencyErrorNotRunning {
export type DependencyErrorNotRunning = {
type: 'notRunning'
}
export interface DependencyErrorIncorrectVersion {
export type DependencyErrorIncorrectVersion = {
type: 'incorrectVersion'
expected: string // version range
received: string // version
}
export interface DependencyErrorConfigUnsatisfied {
export type DependencyErrorConfigUnsatisfied = {
type: 'configUnsatisfied'
}
export interface DependencyErrorHealthChecksFailed {
export type DependencyErrorHealthChecksFailed = {
type: 'healthChecksFailed'
check: T.HealthCheckResult
}
export interface DependencyErrorTransitive {
export type DependencyErrorTransitive = {
type: 'transitive'
}

View File

@@ -3,11 +3,10 @@ import {
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { Log, RPCOptions } from '@start9labs/shared'
import { RPCOptions } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { Observable } from 'rxjs'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { BackupTargetType, Metrics, RR } from './api.types'
import { BackupTargetType, RR } from './api.types'
export abstract class ApiService {
// http
@@ -32,7 +31,7 @@ export abstract class ApiService {
abstract openWebsocket$<T>(
guid: string,
config: RR.WebsocketConfig<T>,
config?: RR.WebsocketConfig<T>,
): Observable<T>
// state
@@ -78,20 +77,13 @@ export abstract class ApiService {
// init
abstract initGetProgress(): Promise<RR.InitGetProgressRes>
abstract initFollowProgress(): Promise<RR.InitFollowProgressRes>
abstract initFollowLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>
// server
abstract openLogsWebsocket$(
config: WebSocketSubjectConfig<Log>,
): Observable<Log>
abstract openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics>
abstract getSystemTime(
params: RR.GetSystemTimeReq,
@@ -119,9 +111,9 @@ export abstract class ApiService {
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>
abstract getServerMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes>
abstract followServerMetrics(
params: RR.FollowServerMetricsReq,
): Promise<RR.FollowServerMetricsRes>
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>

View File

@@ -3,16 +3,15 @@ import {
HttpOptions,
HttpService,
isRpcError,
Log,
Method,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
import { ApiService } from './embassy-api.service'
import { BackupTargetType, Metrics, RR } from './api.types'
import { BackupTargetType, RR } from './api.types'
import { ConfigService } from '../config.service'
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
import { webSocket } from 'rxjs/webSocket'
import { Observable, filter, firstValueFrom } from 'rxjs'
import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
@@ -93,20 +92,10 @@ export class LiveApiService extends ApiService {
}
// websocket
// @TODO Matt which of these 2 APIs should we use?
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
const { location } = this.document.defaultView!
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
const host = location.host
config.url = `${protocol}://${host}/ws${config.url}`
return webSocket(config)
}
openWebsocket$<T>(
guid: string,
config: RR.WebsocketConfig<T>,
config: RR.WebsocketConfig<T> = {},
): Observable<T> {
const { location } = this.document.defaultView!
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
@@ -210,7 +199,7 @@ export class LiveApiService extends ApiService {
// init
async initGetProgress(): Promise<RR.InitGetProgressRes> {
async initFollowProgress(): Promise<RR.InitFollowProgressRes> {
return this.rpcRequest({ method: 'init.subscribe', params: {} })
}
@@ -221,15 +210,6 @@ export class LiveApiService extends ApiService {
}
// server
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
return this.openWebsocket(config)
}
openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics> {
return this.openWebsocket(config)
}
async getSystemTime(
params: RR.GetSystemTimeReq,
@@ -271,9 +251,9 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
}
async getServerMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes> {
async followServerMetrics(
params: RR.FollowServerMetricsReq,
): Promise<RR.FollowServerMetricsRes> {
return this.rpcRequest({ method: 'server.metrics', params })
}

View File

@@ -15,7 +15,7 @@ import {
StateInfo,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { BackupTargetType, Metrics, RR } from './api.types'
import { BackupTargetType, RR } from './api.types'
import { Mock } from './api.fixures'
import {
from,
@@ -35,7 +35,6 @@ import {
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
const PROGRESS: T.FullProgress = {
@@ -84,8 +83,8 @@ export class MockApiService extends ApiService {
async uploadPackage(guid: string, body: Blob): Promise<string> {
await pauseFor(2000)
// @TODO Matt what should this return?
return ''
// @TODO Aiden confirm this is correct
return 'success'
}
async uploadFile(body: Blob): Promise<string> {
@@ -258,7 +257,7 @@ export class MockApiService extends ApiService {
// init
async initGetProgress(): Promise<RR.InitGetProgressRes> {
async initFollowProgress(): Promise<RR.InitFollowProgressRes> {
await pauseFor(250)
return {
progress: PROGRESS,
@@ -276,30 +275,6 @@ export class MockApiService extends ApiService {
// server
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
return interval(50).pipe(
map((_, index) => {
// mock fire open observer
if (index === 0) config.openObserver?.next(new Event(''))
if (index === 100) throw new Error('HAAHHA')
return Mock.ServerLogs[0]
}),
)
}
openMetricsWebsocket$(
config: WebSocketSubjectConfig<Metrics>,
): Observable<Metrics> {
return interval(2000).pipe(
map((_, index) => {
// mock fire open observer
if (index === 0) config.openObserver?.next(new Event(''))
if (index === 4) throw new Error('HAHAHA')
return Mock.getMetrics()
}),
)
}
async getSystemTime(
params: RR.GetSystemTimeReq,
): Promise<RR.GetSystemTimeRes> {
@@ -386,9 +361,9 @@ export class MockApiService extends ApiService {
return logs
}
async getServerMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes> {
async followServerMetrics(
params: RR.FollowServerMetricsReq,
): Promise<RR.FollowServerMetricsRes> {
await pauseFor(2000)
return {
guid: 'iqudh37um-i38u3-34-a51b-jkhd783ein',

View File

@@ -130,8 +130,7 @@ export const mockPatchData: DataModel = {
password: '',
},
platform: 'x86_64-nonfree',
// @TODO Matt zram needs to be added?
// zram: true,
zram: true,
governor: 'performance',
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',

View File

@@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core'
import { ErrorService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import {
NotificationLevel,
ServerNotification,
ServerNotifications,
} from 'src/app/services/api/api.types'
@@ -63,13 +62,13 @@ export class NotificationService {
getColor(notification: ServerNotification<number>): string {
switch (notification.level) {
case NotificationLevel.Info:
case 'info':
return 'var(--tui-status-info)'
case NotificationLevel.Success:
case 'success':
return 'var(--tui-status-positive)'
case NotificationLevel.Warning:
case 'warning':
return 'var(--tui-status-warning)'
case NotificationLevel.Error:
case 'error':
return 'var(--tui-status-negative)'
default:
return ''
@@ -78,12 +77,12 @@ export class NotificationService {
getIcon(notification: ServerNotification<number>): string {
switch (notification.level) {
case NotificationLevel.Info:
case 'info':
return '@tui.info'
case NotificationLevel.Success:
case 'success':
return '@tui.circle-check'
case NotificationLevel.Warning:
case NotificationLevel.Error:
case 'warning':
case 'error':
return '@tui.circle-alert'
default:
return ''

View File

@@ -56,6 +56,7 @@ export type ServerInfo = {
platform: string
arch: string
governor: string | null
zram: boolean
}
export type NetworkInfo = {