Refactor/status info (#3066)

* refactor status info

* wip fe

* frontend changes and version bump

* fix tests and motd

* add registry workflow

* better starttunnel instructions

* placeholders for starttunnel tables

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-12-02 16:31:02 -07:00
committed by GitHub
parent 7c772e873d
commit 3c27499795
80 changed files with 920 additions and 1062 deletions

View File

@@ -26,7 +26,7 @@ const allowedStatuses = {
'restoring',
'stopping',
'starting',
'backingUp',
'backing-up',
]),
}
@@ -45,9 +45,7 @@ export class ActionService {
const { pkgInfo, actionInfo } = data
if (
allowedStatuses[actionInfo.metadata.allowedStatuses].has(
pkgInfo.mainStatus,
)
allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status)
) {
if (actionInfo.metadata.hasInput) {
this.formDialog.open<PackageActionData>(ActionInputModal, {

View File

@@ -1921,8 +1921,9 @@ export namespace Mock {
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/bitcoin-core.svg',
lastBackup: null,
status: {
main: 'running',
statusInfo: {
error: null,
desired: { main: 'running' },
started: new Date().toISOString(),
health: {},
},
@@ -2201,8 +2202,11 @@ export namespace Mock {
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
lastBackup: null,
status: {
main: 'stopped',
statusInfo: {
desired: { main: 'stopped' },
started: null,
health: {},
error: null,
},
actions: {},
serviceInterfaces: {
@@ -2246,8 +2250,11 @@ export namespace Mock {
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
status: {
main: 'stopped',
statusInfo: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
actions: {
config: {

View File

@@ -762,12 +762,12 @@ export class MockApiService extends ApiService {
setTimeout(async () => {
for (let i = 0; i < ids.length; i++) {
const id = ids[i]
const appPath = `/packageData/${id}/status/main/`
const appPatch: ReplaceOperation<T.MainStatus['main']>[] = [
const appPath = `/packageData/${id}/statusInfo/desired/main`
const appPatch: ReplaceOperation<T.DesiredStatus['main']>[] = [
{
op: PatchOp.REPLACE,
path: appPath,
value: 'backingUp',
value: 'backing-up',
},
]
this.mockRevision(appPatch)
@@ -1073,17 +1073,18 @@ export class MockApiService extends ApiService {
}
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
const path = `/packageData/${params.id}/status`
const path = `/packageData/${params.id}/statusInfo`
await pauseFor(2000)
setTimeout(async () => {
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
const patch2: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path,
value: {
main: 'running',
error: null,
desired: { main: 'running' },
started: new Date().toISOString(),
health: {
'ephemeral-health-check': {
@@ -1118,14 +1119,14 @@ export class MockApiService extends ApiService {
this.mockRevision(patch2)
}, 2000)
const originalPatch: ReplaceOperation<
T.MainStatus & { main: 'starting' }
>[] = [
const originalPatch: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path,
value: {
main: 'starting',
desired: { main: 'running' },
started: null,
error: null,
health: {},
},
},
@@ -1140,15 +1141,16 @@ export class MockApiService extends ApiService {
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
await pauseFor(2000)
const path = `/packageData/${params.id}/status`
const path = `/packageData/${params.id}/statusInfo`
setTimeout(async () => {
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
const patch2: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path,
value: {
main: 'running',
desired: { main: 'running' },
error: null,
started: new Date().toISOString(),
health: {
'ephemeral-health-check': {
@@ -1183,12 +1185,15 @@ export class MockApiService extends ApiService {
this.mockRevision(patch2)
}, this.revertTime)
const patch: ReplaceOperation<T.MainStatus & { main: 'restarting' }>[] = [
const patch: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path,
value: {
main: 'restarting',
desired: { main: 'restarting' },
started: null,
error: null,
health: {},
},
},
]
@@ -1200,24 +1205,34 @@ export class MockApiService extends ApiService {
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000)
const path = `/packageData/${params.id}/status`
const path = `/packageData/${params.id}/statusInfo`
setTimeout(() => {
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
const patch2: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path: path,
value: { main: 'stopped' },
value: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
},
]
this.mockRevision(patch2)
}, this.revertTime)
const patch: ReplaceOperation<T.MainStatus & { main: 'stopping' }>[] = [
const patch: ReplaceOperation<T.StatusInfo>[] = [
{
op: PatchOp.REPLACE,
path: path,
value: { main: 'stopping' },
value: {
desired: { main: 'stopped' },
error: null,
health: {},
started: new Date().toISOString(),
},
},
]

View File

@@ -232,8 +232,11 @@ export const mockPatchData: DataModel = {
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/bitcoin-core.svg',
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
status: {
main: 'stopped',
statusInfo: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
// status: {
// main: 'error',
@@ -518,8 +521,11 @@ export const mockPatchData: DataModel = {
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
status: {
main: 'stopped',
statusInfo: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
actions: {
config: {

View File

@@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal'
import { Observable } from 'rxjs'
import { isInstalled } from 'src/app/utils/get-package-data'
import { T } from '@start9labs/start-sdk'
import { getInstalledBaseStatus } from './pkg-status-rendering.service'
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
export type PkgDependencyErrors = Record<string, DependencyError | null>
@@ -153,7 +154,7 @@ export class DepErrorService {
}
}
const depStatus = dep.status.main
const depStatus = getInstalledBaseStatus(dep.statusInfo)
// not running
if (depStatus !== 'running' && depStatus !== 'starting') {
@@ -165,7 +166,7 @@ export class DepErrorService {
// health check failure
if (depStatus === 'running' && currentDep?.kind === 'running') {
for (let id of currentDep.healthChecks) {
const check = dep.status.health[id]
const check = dep.statusInfo.health[id]
if (check && check?.result !== 'success') {
return {
type: 'healthChecksFailed',

View File

@@ -13,7 +13,7 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
if (pkg.stateInfo.state === 'installed') {
primary = getInstalledPrimaryStatus(pkg)
health = getHealthStatus(pkg.status)
health = getHealthStatus(pkg.statusInfo)
} else {
primary = pkg.stateInfo.state
}
@@ -21,33 +21,43 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
return { primary, health }
}
export function getInstalledPrimaryStatus({
tasks,
status,
}: T.PackageDataEntry): PrimaryStatus {
export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
if (
Object.values(tasks).some(t => t.active && t.task.severity === 'critical')
) {
return 'taskRequired'
}
if (
Object.values(status.main === 'running' && status.health)
.filter(h => !!h)
.some(h => h.result === 'starting')
statusInfo.desired.main === 'running' &&
(!statusInfo.started ||
Object.values(statusInfo.health)
.filter(h => !!h)
.some(h => h.result === 'starting'))
) {
return 'starting'
}
return status.main
if (statusInfo.desired.main === 'stopped' && statusInfo.started) {
return 'stopping'
}
return statusInfo.desired.main
}
function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
if (status.main !== 'running' || !status.main) {
export function getInstalledPrimaryStatus({
tasks,
statusInfo,
}: T.PackageDataEntry): PrimaryStatus {
if (
Object.values(tasks).some(t => t.active && t.task.severity === 'critical')
) {
return 'task-required'
}
return getInstalledBaseStatus(statusInfo)
}
function getHealthStatus(statusInfo: T.StatusInfo): T.HealthStatus | null {
if (statusInfo.desired.main !== 'running') {
return null
}
const values = Object.values(status.health).filter(h => !!h)
const values = Object.values(statusInfo.health).filter(h => !!h)
if (values.some(h => h.result === 'failure')) {
return 'failure'
@@ -70,7 +80,7 @@ export interface StatusRendering {
showDots?: boolean
}
export type PrimaryStatus =
export type BaseStatus =
| 'installing'
| 'updating'
| 'removing'
@@ -80,10 +90,11 @@ export type PrimaryStatus =
| 'stopping'
| 'restarting'
| 'stopped'
| 'backingUp'
| 'taskRequired'
| 'backing-up'
| 'error'
export type PrimaryStatus = BaseStatus | 'task-required'
export type DependencyStatus = 'warning' | 'satisfied'
export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
@@ -122,7 +133,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
color: 'dark-shade',
showDots: false,
},
backingUp: {
'backing-up': {
display: 'Backing Up',
color: 'primary',
showDots: true,
@@ -137,7 +148,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
color: 'success',
showDots: false,
},
taskRequired: {
'task-required': {
display: 'Task Required',
color: 'warning',
showDots: false,