Fix/fe bugs 3 (#2943)

* fix typeo in patch db seed

* show all registries in updates tab, fix required dependnecy display in marketplace, update browser tab title desc

* always show pointer for version select

* chore: fix comments

* support html in action desc and marketplace long desc, only show qr in action res if qr is true

* disable save if smtp creds not edited, show better smtp success message

* dont dismiss login spinner until patchDB returns

* feat: redesign of service dashboard and interface (#2946)

* feat: redesign of service dashboard and interface

* chore: comments

* re-add setup complete

* dibale launch UI when not running, re-style things, rename things

* back to 1000

* fix clearnet docs link and require password retype in setup wiz

* faster hint display

* display dependency ID if title not available

* fix migration

* better init progress view

* fix setup success page by providing VERSION and notifications page fixes

* force uninstall from service error page, soft or hard

* handle error state better

* chore: fixed for install and setup wizards

* chore: fix issues (#2949)

* enable and disable kiosk mode

* minor fixes

* fix dependency mounts

* dismissable tasks

* provide replayId

* default if health check success message is null

* look for wifi interface too

* dash for null user agent in sessions

* add disk repair to diagnostic api

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-05-21 19:04:26 -06:00
committed by GitHub
parent 44560c8da8
commit b40849f672
123 changed files with 1662 additions and 964 deletions

View File

@@ -18,7 +18,7 @@ const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = {
const mockDescription = {
short: 'Lorem ipsum dolor sit amet',
long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
long: 'Lorem ipsum dolor sit amet, <p>consectetur adipiscing elit</p>, sed do eiusmod <i>tempor</i> incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
}
export namespace Mock {
@@ -632,55 +632,6 @@ export namespace Mock {
},
},
},
'btc-rpc-proxy': {
'=0.3.2.6:0': {
best: {
'0.3.2.6:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues',
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,
dependencyMetadata: {
bitcoind: {
title: 'Bitcoin Core',
icon: BTC_ICON,
description: 'Used for RPC requests',
optional: false,
},
},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin'],
otherVersions: {
'0.3.2.7:0': {
releaseNotes: 'Upstream release and minor fixes.',
},
},
},
},
}
export const RegistryPackages: GetPackagesRes = {
@@ -857,11 +808,7 @@ export namespace Mock {
},
},
categories: ['bitcoin'],
otherVersions: {
'0.3.2.6:0': {
releaseNotes: 'Upstream release and minor fixes.',
},
},
otherVersions: {},
},
}
@@ -891,7 +838,7 @@ export namespace Mock {
id: 2,
packageId: null,
createdAt: '2019-12-26T14:20:30.872Z',
code: 2,
code: 0,
level: 'warning',
title: 'SSH Key Added',
message: 'A new SSH key was added. If you did not do this, shit is bad.',
@@ -902,7 +849,7 @@ export namespace Mock {
id: 3,
packageId: null,
createdAt: '2019-12-26T14:20:30.872Z',
code: 3,
code: 0,
level: 'info',
title: 'SSH Key Removed',
message: 'A SSH key was removed.',
@@ -913,7 +860,7 @@ export namespace Mock {
id: 4,
packageId: 'bitcoind',
createdAt: '2019-12-26T14:20:30.872Z',
code: 4,
code: 0,
level: 'error',
title: 'Service Crashed',
message: new Array(3)
@@ -1339,7 +1286,7 @@ export namespace Mock {
result: {
type: 'single',
copyable: true,
qr: true,
qr: false,
masked: true,
value: 'iwejdoiewdhbew',
},

View File

@@ -335,6 +335,12 @@ export namespace RR {
} // package.action.run
export type ActionRes = (T.ActionResult & { version: '1' }) | null
export type ClearTaskReq = {
packageId: string
replayId: string
} // package.action.clear-task
export type ClearTaskRes = null
export type RestorePackagesReq = {
// package.backup.restore
ids: string[]
@@ -356,7 +362,11 @@ export namespace RR {
export type RebuildPackageReq = { id: string } // package.rebuild
export type RebuildPackageRes = null
export type UninstallPackageReq = { id: string } // package.uninstall
export type UninstallPackageReq = {
id: string
force: boolean
soft: boolean
} // package.uninstall
export type UninstallPackageRes = null
export type SideloadPackageReq = {

View File

@@ -120,6 +120,8 @@ export abstract class ApiService {
abstract repairDisk(params: RR.DiskRepairReq): Promise<RR.DiskRepairRes>
abstract toggleKiosk(enable: boolean): Promise<null>
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
// @TODO 041
@@ -335,6 +337,8 @@ export abstract class ApiService {
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
abstract clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes>
abstract restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes>

View File

@@ -261,6 +261,13 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'disk.repair', params })
}
async toggleKiosk(enable: boolean): Promise<null> {
return this.rpcRequest({
method: enable ? 'kiosk.enable' : 'kiosk.disable',
params: {},
})
}
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
return this.rpcRequest({ method: 'net.tor.reset', params })
}
@@ -577,6 +584,10 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.action.run', params })
}
async clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes> {
return this.rpcRequest({ method: 'package.action.clear-task', params })
}
async restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {

View File

@@ -22,11 +22,7 @@ import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
import { mockPatchData } from './mock-patch'
import { AuthService } from '../auth.service'
import { T } from '@start9labs/start-sdk'
import {
GetPackageRes,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { MarketplacePkg } from '@start9labs/marketplace'
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
import { WebSocketSubject } from 'rxjs/webSocket'
import { toAcmeUrl } from 'src/app/utils/acme'
@@ -166,7 +162,6 @@ export class MockApiService extends ApiService {
pathArr: Array<string | number>,
value: T,
): Promise<RR.SetDBValueRes> {
console.warn(pathArr, value)
const pointer = pathFromArray(pathArr)
const params: RR.SetDBValueReq<T> = { pointer, value }
await pauseFor(2000)
@@ -449,6 +444,21 @@ export class MockApiService extends ApiService {
return null
}
async toggleKiosk(enable: boolean): Promise<null> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/serverInfo/kiosk',
value: enable,
},
]
this.mockRevision(patch)
return null
}
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
await pauseFor(2000)
return null
@@ -1103,23 +1113,32 @@ export class MockApiService extends ApiService {
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
await pauseFor(2000)
if (params.actionId === 'properties') {
// return Mock.ActionResGroup
return Mock.ActionResMessage
// return Mock.ActionResSingle
} else if (params.actionId === 'config') {
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.packageId}/requestedActions/${params.packageId}-config`,
},
]
this.mockRevision(patch)
return null
} else {
return Mock.ActionResMessage
// return Mock.ActionResSingle
}
const patch: ReplaceOperation<{ [key: string]: T.TaskEntry }>[] = [
{
op: PatchOp.REPLACE,
path: `/packageData/${params.packageId}/tasks`,
value: {},
},
]
this.mockRevision(patch)
// return Mock.ActionResGroup
return Mock.ActionResMessage
// return Mock.ActionResSingle
}
async clearTask(params: RR.ClearTaskReq): Promise<RR.ClearTaskRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.packageId}/tasks/${params.replayId}`,
},
]
this.mockRevision(patch)
return null
}
async restorePackages(

View File

@@ -183,13 +183,7 @@ export const mockPatchData: DataModel = {
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15',
ntpSynced: false,
smtp: {
server: '',
port: 587,
from: '',
login: '',
password: '',
},
smtp: null,
platform: 'x86_64-nonfree',
zram: true,
governor: 'performance',
@@ -221,7 +215,7 @@ export const mockPatchData: DataModel = {
actions: {
config: {
name: 'Set Config',
description: 'edit bitcoin.conf',
description: 'edit bitcoin.conf, <b>soo cool!</b>',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',

View File

@@ -1,22 +1,18 @@
import { Injectable } from '@angular/core'
import { inject, Injectable } from '@angular/core'
import { ReplaySubject } from 'rxjs'
import { StorageService } from './storage.service'
const SHOW_DEV_TOOLS = 'SHOW_DEV_TOOLS'
const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
@Injectable({
providedIn: 'root',
})
export class ClientStorageService {
private readonly storage = inject(StorageService)
readonly showDevTools$ = new ReplaySubject<boolean>(1)
readonly showDiskRepair$ = new ReplaySubject<boolean>(1)
constructor(private readonly storage: StorageService) {}
init() {
this.showDevTools$.next(!!this.storage.get(SHOW_DEV_TOOLS))
this.showDiskRepair$.next(!!this.storage.get(SHOW_DISK_REPAIR))
}
toggleShowDevTools(): boolean {
@@ -25,11 +21,4 @@ export class ClientStorageService {
this.showDevTools$.next(newVal)
return newVal
}
toggleShowDiskRepair(): boolean {
const newVal = !this.storage.get(SHOW_DISK_REPAIR)
this.storage.set(SHOW_DISK_REPAIR, newVal)
this.showDiskRepair$.next(newVal)
return newVal
}
}

View File

@@ -31,7 +31,6 @@ import {
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { ClientStorageService } from './client-storage.service'
const { start9, community } = defaultRegistries
@@ -55,20 +54,6 @@ export class MarketplaceService {
]),
)
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
readonly filteredRegistries$: Observable<StoreIdentity[]> = combineLatest([
inject(ClientStorageService).showDevTools$,
this.registries$,
]).pipe(
map(([devMode, registries]) =>
devMode
? registries
: registries.filter(
({ url }) => !url.includes('alpha') && !url.includes('beta'),
),
),
)
readonly currentRegistryUrl$ = new ReplaySubject<string>(1)
readonly requestErrors$ = new BehaviorSubject<string[]>([])
@@ -252,7 +237,6 @@ export class MarketplaceService {
oldName: string | null,
newName: string,
): Promise<void> {
console.warn(oldName, newName)
if (oldName !== newName) {
this.api.setDbValue<string>(['registries', url], newName)
}

View File

@@ -93,7 +93,7 @@ export class NotificationService {
{ data, createdAt, code, title, message }: ServerNotification<number>,
full = false,
) {
const label = full || code === 2 ? title : 'Backup Report'
const label = code === 1 ? 'Backup Report' : title
const component = code === 1 ? REPORT : MARKDOWN
const content = code === 1 ? data : of(data)
@@ -104,6 +104,7 @@ export class NotificationService {
content,
timestamp: createdAt,
},
size: code === 1 ? 'm' : 'l',
})
.subscribe()
}

View File

@@ -1,31 +1,24 @@
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PkgDependencyErrors } from './dep-error.service'
import { T } from '@start9labs/start-sdk'
import { i18nKey } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
export interface PackageStatus {
primary: PrimaryStatus
dependency: DependencyStatus | null
health: T.HealthStatus | null
}
export function renderPkgStatus(
pkg: PackageDataEntry,
depErrors: PkgDependencyErrors = {},
): PackageStatus {
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
let primary: PrimaryStatus
let dependency: DependencyStatus | null = null
let health: T.HealthStatus | null = null
if (pkg.stateInfo.state === 'installed') {
primary = getInstalledPrimaryStatus(pkg)
dependency = getDependencyStatus(depErrors)
health = getHealthStatus(pkg.status)
} else {
primary = pkg.stateInfo.state
}
return { primary, dependency, health }
return { primary, health }
}
export function getInstalledPrimaryStatus({
@@ -39,10 +32,6 @@ export function getInstalledPrimaryStatus({
: status.main
}
function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
return Object.values(depErrors).some(err => !!err) ? 'warning' : 'satisfied'
}
function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
if (status.main !== 'running' || !status.main) {
return null

View File

@@ -14,6 +14,7 @@ import { getAllPackages } from '../utils/get-package-data'
import { hasCurrentDeps } from '../utils/has-deps'
import { ApiService } from './api/embassy-api.service'
import { DataModel } from './patch-db/data-model'
import { RR } from './api/api.types'
@Injectable({
providedIn: 'root',
@@ -40,13 +41,20 @@ export class StandardActionsService {
}
}
async uninstall({ id, title, alerts }: T.Manifest): Promise<void> {
let content =
alerts.uninstall ||
`${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
async uninstall(
{ id, title, alerts }: T.Manifest,
{ force, soft }: { force: boolean; soft: boolean } = {
force: false,
soft: false,
},
): Promise<void> {
let content = soft
? ''
: alerts.uninstall ||
`${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
content = `${content}. ${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
}
this.dialog
@@ -60,17 +68,15 @@ export class StandardActionsService {
},
})
.pipe(filter(Boolean))
.subscribe(() => this.doUninstall(id))
.subscribe(() => this.doUninstall({ id, force, soft }))
}
private async doUninstall(id: string) {
private async doUninstall(options: RR.UninstallPackageReq) {
const loader = this.loader.open('Beginning uninstall').subscribe()
try {
await this.api.uninstallPackage({ id })
await this.api
.setDbValue<boolean>(['ackInstructions', id], false)
.catch(e => console.error('Failed to mark instructions as unseen', e))
await this.api.uninstallPackage(options)
await this.api.setDbValue<boolean>(['ackInstructions', options.id], false)
await this.router.navigate(['portal'])
} catch (e: any) {
this.errorService.handleError(e)