Feature/tor logs (#3077)

* add tor logs, rework services page, other small things

* feat: sortable service table and mobile view

---------

Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2025-12-16 12:47:43 -07:00
committed by GitHub
parent e35b643e51
commit 5d8331b7f7
26 changed files with 443 additions and 506 deletions

View File

@@ -88,6 +88,8 @@ export default {
88: 'Aktionen',
89: 'nicht empfohlen',
90: 'Root-CA ist vertrauenswürdig!',
91: 'Installierte Dienste',
92: 'Diagnosen für den Tor-Daemon auf diesem Server',
96: 'Öffentliche Domain hinzufügen',
97: 'Wird entfernt',
100: 'Nicht gespeicherte Änderungen',

View File

@@ -87,6 +87,8 @@ export const ENGLISH = {
'Actions': 88, // as in, actions available to the user
'not recommended': 89,
'Root CA Trusted!': 90,
'Installed services': 91, // as in, software services installed on this computer
'Diagnostics for the Tor daemon on this server': 92,
'Add public domain': 96,
'Removing': 97,
'Unsaved changes': 100,

View File

@@ -88,6 +88,8 @@ export default {
88: 'Acciones',
89: 'no recomendado',
90: '¡CA raíz confiable!',
91: 'Servicios instalados',
92: 'Diagnósticos para el demonio Tor en este servidor',
96: 'Agregar dominio público',
97: 'Eliminando',
100: 'Cambios no guardados',

View File

@@ -88,6 +88,8 @@ export default {
88: 'Actions',
89: 'non recommandé',
90: 'Certificat racine approuvé !',
91: 'Services installés',
92: 'Diagnostics pour le service Tor sur ce serveur',
96: 'Ajouter un domaine public',
97: 'Suppression',
100: 'Modifications non enregistrées',

View File

@@ -88,6 +88,8 @@ export default {
88: 'Akcje',
89: 'niezalecane',
90: 'Główny certyfikat CA zaufany!',
91: 'Zainstalowane usługi',
92: 'Diagnostyka demona Tor na tym serwerze',
96: 'Dodaj domenę publiczną',
97: 'Usuwanie',
100: 'Niezapisane zmiany',

View File

@@ -3,7 +3,12 @@ import { TuiIcon } from '@taiga-ui/core'
@Component({
selector: 'app-placeholder',
template: '<tui-icon [icon]="icon()" /><ng-content/>',
template: `
@if (icon(); as icon) {
<tui-icon [icon]="icon" />
}
<ng-content />
`,
styles: `
:host {
display: flex;
@@ -26,5 +31,5 @@ import { TuiIcon } from '@taiga-ui/core'
imports: [TuiIcon],
})
export class PlaceholderComponent {
readonly icon = input.required<string>()
readonly icon = input<string>()
}

View File

@@ -1,5 +1,10 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nKey, i18nPipe } from '@start9labs/shared'
import {
TuiComparator,
TuiTable,
TuiTableDirective,
} from '@taiga-ui/addon-table'
@Component({
selector: 'table[appTable]',
@@ -8,7 +13,13 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
<tr>
<ng-content select="th" />
@for (header of appTable(); track $index) {
<th>{{ header | i18n }}</th>
<th
tuiTh
[requiredSort]="true"
[sorter]="appTableSorters()[$index] || null"
>
{{ header | i18n }}
</th>
}
</tr>
</thead>
@@ -22,9 +33,16 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
}
`,
host: { class: 'g-table' },
hostDirectives: [
{
directive: TuiTableDirective,
inputs: ['sorter'],
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [i18nPipe],
imports: [i18nPipe, TuiTable],
})
export class TableComponent {
readonly appTable = input.required<ReadonlyArray<i18nKey | null>>()
readonly appTableSorters = input<ReadonlyArray<TuiComparator<any> | null>>([])
}

View File

@@ -13,6 +13,10 @@ export const ROUTES: Routes = [
path: 'os',
loadComponent: () => import('./routes/os.component'),
},
{
path: 'tor',
loadComponent: () => import('./routes/tor.component'),
},
]
export default ROUTES

View File

@@ -79,6 +79,12 @@ export default class SystemLogsComponent {
subtitle: 'Raw, unfiltered operating system logs',
icon: '@tui.square-dashed-bottom-code',
},
{
link: 'tor',
title: 'Tor Logs',
subtitle: 'Diagnostics for the Tor daemon on this server',
icon: '@tui.target',
},
{
link: 'kernel',
title: 'Kernel Logs',

View File

@@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@Component({
template: `
<logs-header [title]="'Tor Logs' | i18n">
{{ 'Diagnostics for the Tor daemon on this server' | i18n }}
</logs-header>
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
`,
styles: `
:host {
padding: 1rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
host: { class: 'g-page' },
})
export default class SystemTorComponent {
private readonly api = inject(ApiService)
protected readonly follow = (params: RR.FollowServerLogsReq) =>
this.api.followTorLogs(params)
protected readonly fetch = (params: RR.GetServerLogsReq) =>
this.api.getTorLogs(params)
}

View File

@@ -167,6 +167,9 @@ export default class MarketplaceComponent {
takeUntilDestroyed(),
tap(params => {
const registry = params.get('registry')
this.categoryService.setQuery(params.get('search') || '')
if (!registry) {
this.router.navigate([], {
queryParams: {

View File

@@ -49,16 +49,6 @@ import { NotificationsTableComponent } from './table.component'
:host {
padding: 1rem;
}
:host-context(tui-root._mobile) {
header {
display: none;
}
section {
padding-block: 0;
}
}
`,
host: { class: 'g-page' },
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -12,6 +12,7 @@ import { ServerNotification } from 'src/app/services/api/api.types'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { NotificationItemComponent } from './item.component'
import { i18nPipe } from '@start9labs/shared'
import { PlaceholderComponent } from '../../components/placeholder.component'
@Component({
selector: '[notifications]',
@@ -48,9 +49,9 @@ import { i18nPipe } from '@start9labs/shared'
</tr>
} @empty {
@if (notifications()) {
<tr>
<td colspan="4">{{ 'No notifications' | i18n }}</td>
</tr>
<app-placeholder icon="@tui.bell">
{{ 'No notifications' | i18n }}
</app-placeholder>
} @else {
@for (i of ['', '']; track $index) {
<tr>
@@ -71,10 +72,6 @@ import { i18nPipe } from '@start9labs/shared'
transform: translateY(-50%);
}
td:only-child {
text-align: center;
}
:host-context(tui-root._mobile) {
input {
position: absolute;
@@ -97,6 +94,7 @@ import { i18nPipe } from '@start9labs/shared'
TuiSkeleton,
i18nPipe,
TableComponent,
PlaceholderComponent,
],
})
export class NotificationsTableComponent<T extends ServerNotification<number>>

View File

@@ -18,7 +18,6 @@ import { ToManifestPipe } from '../../../pipes/to-manifest'
@let services = this.services();
@for (d of pkg().currentDependencies | keyvalue; track $index) {
<!-- @TODO Alex Marketplace should use "search" query param to prefill search bar -->
<a
tuiCell
[routerLink]="services[d.key] ? ['..', d.key] : ['/marketplace']"

View File

@@ -86,7 +86,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
td:last-child {
white-space: nowrap;
text-align: right;
justify-content: end;
display: flex;
gap: 8px;
}

View File

@@ -1,80 +0,0 @@
// import { AsyncPipe } from '@angular/common'
// import {
// ChangeDetectionStrategy,
// Component,
// computed,
// inject,
// input,
// } from '@angular/core'
// import { i18nPipe } from '@start9labs/shared'
// import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
// import { map } from 'rxjs'
// import { ControlsService } from 'src/app/services/controls.service'
// import { DepErrorService } from 'src/app/services/dep-error.service'
// import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
// import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
// import { getManifest } from 'src/app/utils/get-package-data'
// import { UILaunchComponent } from './ui-launch.component'
// const RUNNING = ['running', 'starting', 'restarting']
// @Component({
// selector: 'fieldset[appControls]',
// template: `
// <app-ui-launch [pkg]="pkg()" />
// @if (running()) {
// <button
// tuiIconButton
// iconStart="@tui.square"
// (click)="controls.stop(manifest())"
// >
// {{ 'Stop' | i18n }}
// </button>
// } @else {
// @let unmet = hasUnmet() | async;
// <button
// tuiIconButton
// iconStart="@tui.play"
// [disabled]="status().primary !== 'stopped'"
// (click)="controls.start(manifest(), !!unmet)"
// >
// {{ 'Start' | i18n }}
// </button>
// }
// `,
// changeDetection: ChangeDetectionStrategy.OnPush,
// imports: [TuiButton, UILaunchComponent, AsyncPipe, i18nPipe],
// providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
// styles: `
// :host {
// padding: 0;
// border: none;
// cursor: default;
// text-align: right;
// }
// :host-context(tui-root._mobile) {
// button {
// display: none;
// }
// }
// `,
// })
// export class ControlsComponent {
// private readonly errors = inject(DepErrorService)
// readonly controls = inject(ControlsService)
// readonly pkg = input.required<PackageDataEntry>()
// readonly status = computed(() => renderPkgStatus(this.pkg()))
// readonly running = computed(() => RUNNING.includes(this.status().primary))
// readonly manifest = computed(() => getManifest(this.pkg()))
// readonly hasUnmet = computed(() =>
// this.errors.getPkgDepErrors$(this.manifest().id).pipe(
// map(errors =>
// Object.keys(this.pkg().currentDependencies)
// .map(id => errors?.[id])
// .some(Boolean),
// ),
// ),
// )
// }

View File

@@ -1,160 +1,61 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import {
ChangeDetectionStrategy,
Component,
inject,
viewChild,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
import { TuiButton, TuiLoader } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { map, shareReplay } from 'rxjs'
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
import { DepErrorService } from 'src/app/services/dep-error.service'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
import { getManifest } from 'src/app/utils/get-package-data'
import { ServiceComponent } from './service.component'
import { ServicesTableComponent } from './table.component'
@Component({
template: `
<ng-container *title>{{ 'Services' | i18n }}</ng-container>
@if (!services()) {
<tui-loader [style.height.%]="100" [textContent]="'Loading' | i18n" />
} @else {
@if (services()?.length) {
<table tuiTable class="g-table" [(sorter)]="sorter">
<thead>
<tr>
<th [style.width.rem]="3"></th>
<th tuiTh [requiredSort]="true" [sorter]="name">
{{ 'Name' | i18n }}
</th>
<th tuiTh [requiredSort]="true" [sorter]="status">
{{ 'Status' | i18n }}
</th>
<th tuiTh>{{ 'Version' | i18n }}</th>
<th
tuiTh
[requiredSort]="true"
[sorter]="uptime"
[style.width.rem]="10"
>
{{ 'Uptime' | i18n }}
</th>
</tr>
</thead>
<tbody>
@for (pkg of services() | tuiTableSort; track $index) {
<tr
appService
[pkg]="pkg"
[depErrors]="errors()?.[(pkg | toManifest).id]"
></tr>
}
</tbody>
</table>
} @else {
<section>
<div>
{{ 'Welcome to' | i18n }}
<span>StartOS</span>
</div>
<p>
{{
'To get started, visit the Marketplace and download your first service'
| i18n
}}
</p>
<a tuiButton routerLink="../marketplace">
{{ 'View Marketplace' | i18n }}
</a>
</section>
}
}
<ng-container *title>{{ 'Installed services' | i18n }}</ng-container>
<section class="g-card">
<header>
{{ 'Installed services' | i18n }}
</header>
<div #table [services]="services()"></div>
</section>
`,
styles: `
:host {
position: relative;
font-size: 1rem;
}
table {
max-width: 60rem;
margin: 0 auto;
padding: 1rem;
}
:host-context(tui-root._mobile) {
padding: 0;
}
section {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
div {
font-size: min(12vw, 4rem);
line-height: normal;
header {
display: none;
}
p {
font-size: 1.5rem;
line-height: 1.25em;
padding: 0 1rem;
}
span {
color: #ff4961;
}
a {
margin-block-start: 1rem;
.g-card {
padding: 0;
margin-top: -0.75rem;
background: none;
box-shadow: none;
}
}
`,
host: { class: 'g-page' },
imports: [
ServiceComponent,
ToManifestPipe,
TuiTable,
TitleDirective,
i18nPipe,
TuiLoader,
TuiButton,
RouterLink,
],
imports: [TitleDirective, i18nPipe, ServicesTableComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class DashboardComponent {
readonly errors = toSignal(inject(DepErrorService).depErrors$)
readonly services = toSignal(
inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs => Object.values(pkgs).sort(byName)),
map(pkgs => Object.values(pkgs)),
shareReplay(1),
),
{ initialValue: null },
)
readonly name: TuiComparator<PackageDataEntry> = byName
readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
readonly uptime: TuiComparator<any> = (a, b) =>
a.status.started || '' > a.status.started || '' ? -1 : 1
sorter = this.name
}
function byName(a: PackageDataEntry, b: PackageDataEntry) {
return getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1
: 1
protected _ = viewChild<ServicesTableComponent<any>>('table')
}

View File

@@ -18,18 +18,15 @@ import { StatusComponent } from './status.component'
@Component({
selector: 'tr[appService]',
template: `
<td [style.grid-area]="'1 / 1 / 4'">
<td [style.width.rem]="3" [style.grid-area]="'1 / 1 / 4'">
<img alt="logo" [src]="pkg.icon" />
</td>
<td class="title">
<a [routerLink]="routerLink">{{ manifest.title }}</a>
</td>
<td
appStatus
[pkg]="pkg"
[hasDepErrors]="hasError(depErrors)"
[style.grid-area]="'3 / 2'"
></td>
<td [style.grid-area]="'3 / 2'">
<app-status [pkg]="pkg" [hasDepErrors]="hasError(depErrors)" />
</td>
<td class="version">{{ manifest.version }}</td>
<td class="uptime">
@if (pkg.statusInfo.started; as started) {
@@ -45,7 +42,6 @@ import { StatusComponent } from './status.component'
:host {
@include taiga.transition(background);
clip-path: inset(0 round 0.5rem);
cursor: pointer;
&:hover {
@@ -76,10 +72,14 @@ import { StatusComponent } from './status.component'
:host-context(tui-root._mobile) {
position: relative;
display: grid;
grid-template: 1.25rem 1.5rem 1.5rem/4rem 1fr;
grid-template: 1.25rem 2rem 1.5rem/4rem 1fr;
align-items: center;
padding: 1rem;
&:hover {
background: none;
}
img {
height: 3rem;
width: 3rem;

View File

@@ -15,12 +15,12 @@ import {
} from 'src/app/services/pkg-status-rendering.service'
@Component({
selector: 'td[appStatus]',
selector: 'app-status',
template: `
@if (error()) {
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
} @else if (loading()) {
<tui-loader size="m" />
<tui-loader size="s" />
}
<b [style.color]="color()">{{ statusText() | i18n }}</b>
@@ -33,13 +33,14 @@ import {
:host {
display: flex;
align-items: center;
gap: 0.5rem;
height: 3rem;
gap: 0.25rem;
white-space: nowrap;
}
:host-context(tui-root._mobile) {
height: auto;
tui-icon {
font-size: 1rem;
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -0,0 +1,111 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { T } from '@start9labs/start-sdk'
import {
PackageDataEntry,
StateInfo,
} from 'src/app/services/patch-db/data-model'
import { ServiceComponent } from './service.component'
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data'
import { ToManifestPipe } from '../../../pipes/to-manifest'
import { toSignal } from '@angular/core/rxjs-interop'
import { DepErrorService } from 'src/app/services/dep-error.service'
import { i18nPipe } from '@start9labs/shared'
import { TuiSkeleton } from '@taiga-ui/kit'
import { PlaceholderComponent } from '../../../components/placeholder.component'
import { TuiButton } from '@taiga-ui/core'
import { RouterLink } from '@angular/router'
@Component({
selector: '[services]',
template: `
@if (services()?.length === 0) {
<app-placeholder>
<h1 [style.margin-bottom]="0">
{{ 'Welcome to' | i18n }}
<span>StartOS!</span>
</h1>
<p>
{{
'To get started, visit the Marketplace and download your first service'
| i18n
}}
</p>
<a
style="margin: 1.5rem 0;"
tuiButton
size="m"
iconStart="@tui.shopping-cart"
routerLink="../marketplace"
>
{{ 'View Marketplace' | i18n }}
</a>
</app-placeholder>
} @else {
<table
[sorter]="name"
[appTable]="[null, 'Name', 'Status', 'Version', 'Uptime']"
[appTableSorters]="[null, name, status]"
>
@for (service of services() | tuiTableSort; track service) {
<tr
appService
[pkg]="service"
[depErrors]="errors()?.[(service | toManifest).id]"
></tr>
} @empty {
@for (_ of ['', '']; track $index) {
<tr>
<td colspan="5">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
</td>
</tr>
}
}
</table>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FormsModule,
TableComponent,
ServiceComponent,
ToManifestPipe,
i18nPipe,
TuiSkeleton,
PlaceholderComponent,
TuiButton,
RouterLink,
TuiTable,
],
})
export class ServicesTableComponent<
T extends T.PackageDataEntry & {
stateInfo: StateInfo
},
> {
readonly errors = toSignal(inject(DepErrorService).depErrors$)
readonly services = input.required<readonly T[] | null>()
readonly name: TuiComparator<PackageDataEntry> = byName
readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
}
function byName(a: PackageDataEntry, b: PackageDataEntry) {
return getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1
: 1
}

View File

@@ -1,98 +0,0 @@
// import {
// ChangeDetectionStrategy,
// Component,
// inject,
// Input,
// DOCUMENT,
// } from '@angular/core'
// import { i18nPipe } from '@start9labs/shared'
// import { T } from '@start9labs/start-sdk'
// import { tuiPure } from '@taiga-ui/cdk'
// import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
// import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
// import { InterfaceService } from '../../../components/interfaces/interface.service'
// import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
// @Component({
// selector: 'app-ui-launch',
// template: `
// @if (interfaces.length > 1) {
// <button
// tuiIconButton
// iconStart="@tui.external-link"
// tuiDropdownOpen
// [disabled]="!isRunning"
// [tuiDropdown]="content"
// >
// {{ 'Open' | i18n }}
// </button>
// <ng-template #content>
// <tui-data-list>
// @for (interface of interfaces; track $index) {
// <a
// tuiOption
// target="_blank"
// rel="noreferrer"
// [attr.href]="getHref(interface)"
// >
// {{ interface.name }}
// </a>
// }
// </tui-data-list>
// </ng-template>
// } @else if (interfaces[0]) {
// <button
// tuiIconButton
// iconStart="@tui.external-link"
// [disabled]="!isRunning"
// (click)="openUI(interfaces[0])"
// >
// {{ interfaces[0].name }}
// </button>
// }
// `,
// styles: `
// :host-context(tui-root._mobile) *::before {
// font-size: 1.5rem !important;
// }
// `,
// changeDetection: ChangeDetectionStrategy.OnPush,
// imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe],
// })
// export class UILaunchComponent {
// private readonly interfaceService = inject(InterfaceService)
// private readonly document = inject(DOCUMENT)
// @Input()
// pkg!: PackageDataEntry
// get interfaces(): readonly T.ServiceInterface[] {
// return this.getInterfaces(this.pkg)
// }
// get isRunning(): boolean {
// return getInstalledPrimaryStatus(this.pkg) === 'running'
// }
// @tuiPure
// getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
// return pkg
// ? Object.values(pkg.serviceInterfaces).filter(
// i =>
// i.type === 'ui' &&
// (i.addressInfo.scheme === 'http' ||
// i.addressInfo.sslScheme === 'https'),
// )
// : []
// }
// getHref(ui: T.ServiceInterface): string {
// const host = this.pkg.hosts[ui.addressInfo.hostId]
// if (!host) return ''
// return this.interfaceService.launchableAddress(ui, host)
// }
// openUI(ui: T.ServiceInterface) {
// this.document.defaultView?.open(this.getHref(ui), '_blank', 'noreferrer')
// }
// }

View File

@@ -73,14 +73,14 @@ export namespace RR {
uptime: number // seconds
}
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & net.tor.logs
export type GetServerLogsRes = FetchLogsRes
export type FollowServerLogsReq = {
limit?: number // (optional) default is 50. Ignored if cursor provided
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
cursor?: string // the last known log. Websocket will return all logs since this log
} // server.logs.follow & server.kernel-logs.follow
} // server.logs.follow & server.kernel-logs.follow & net.tor.follow-logs
export type FollowServerLogsRes = {
startCursor: string
guid: string

View File

@@ -86,6 +86,8 @@ export abstract class ApiService {
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes>
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
abstract getKernelLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes>
@@ -94,6 +96,10 @@ export abstract class ApiService {
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>
abstract followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>
abstract followKernelLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>

View File

@@ -206,6 +206,10 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.logs', params })
}
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
return this.rpcRequest({ method: 'net.tor.logs', params })
}
async getKernelLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {
@@ -218,6 +222,12 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.logs.follow', params })
}
async followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
}
async followKernelLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {

View File

@@ -290,6 +290,17 @@ export class MockApiService extends ApiService {
}
}
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
await pauseFor(2000)
const entries = this.randomLogs(params.limit)
return {
entries,
startCursor: 'start-cursor',
endCursor: 'end-cursor',
}
}
async getKernelLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {
@@ -313,6 +324,16 @@ export class MockApiService extends ApiService {
}
}
async followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {
await pauseFor(2000)
return {
startCursor: 'start-cursor',
guid: 'logs-guid',
}
}
async followKernelLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {

View File

@@ -222,6 +222,167 @@ export const mockPatchData: DataModel = {
kiosk: true,
},
packageData: {
lnd: {
stateInfo: {
state: 'installed',
manifest: {
...Mock.MockManifestLnd,
version: '0.11.0:0.0.1',
},
},
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
statusInfo: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: 'Connecting',
},
},
serviceInterfaces: {
grpc: {
id: 'grpc',
masked: false,
name: 'GRPC',
description:
'Used by dependent services and client wallets for connecting to your node',
type: 'api',
addressInfo: {
username: null,
hostId: 'qrstuv',
internalPort: 10009,
scheme: null,
sslScheme: 'grpc',
suffix: '',
},
},
lndconnect: {
id: 'lndconnect',
masked: true,
name: 'LND Connect',
description:
'Used by client wallets adhering to LND Connect protocol to connect to your node',
type: 'api',
addressInfo: {
username: null,
hostId: 'qrstuv',
internalPort: 10009,
scheme: null,
sslScheme: 'lndconnect',
suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand',
},
},
p2p: {
id: 'p2p',
masked: false,
name: 'P2P',
description:
'Used for connecting to other nodes on the Bitcoin network',
type: 'p2p',
addressInfo: {
username: null,
hostId: 'rstuvw',
internalPort: 8333,
scheme: 'bitcoin',
sslScheme: null,
suffix: '',
},
},
},
currentDependencies: {
bitcoind: {
title: Mock.BitcoinDep.title,
icon: Mock.BitcoinDep.icon,
kind: 'running',
versionRange: '>=26.0.0',
healthChecks: [],
},
'btc-rpc-proxy': {
title: Mock.ProxyDep.title,
icon: Mock.ProxyDep.icon,
kind: 'running',
versionRange: '>2.0.0',
healthChecks: [],
},
},
hosts: {},
storeExposedDependents: [],
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
tasks: {
config: {
active: true,
task: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
task: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': {
active: true,
task: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason: 'LND likes BTC a certain way',
input: {
kind: 'partial',
value: {
color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
task: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: {
rpcuser: 'lnd',
},
},
},
},
},
},
},
bitcoind: {
stateInfo: {
state: 'installed',
@@ -511,166 +672,5 @@ export const mockPatchData: DataModel = {
},
},
},
lnd: {
stateInfo: {
state: 'installed',
manifest: {
...Mock.MockManifestLnd,
version: '0.11.0:0.0.1',
},
},
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
statusInfo: {
desired: { main: 'stopped' },
error: null,
health: {},
started: null,
},
actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: 'Connecting',
},
},
serviceInterfaces: {
grpc: {
id: 'grpc',
masked: false,
name: 'GRPC',
description:
'Used by dependent services and client wallets for connecting to your node',
type: 'api',
addressInfo: {
username: null,
hostId: 'qrstuv',
internalPort: 10009,
scheme: null,
sslScheme: 'grpc',
suffix: '',
},
},
lndconnect: {
id: 'lndconnect',
masked: true,
name: 'LND Connect',
description:
'Used by client wallets adhering to LND Connect protocol to connect to your node',
type: 'api',
addressInfo: {
username: null,
hostId: 'qrstuv',
internalPort: 10009,
scheme: null,
sslScheme: 'lndconnect',
suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand',
},
},
p2p: {
id: 'p2p',
masked: false,
name: 'P2P',
description:
'Used for connecting to other nodes on the Bitcoin network',
type: 'p2p',
addressInfo: {
username: null,
hostId: 'rstuvw',
internalPort: 8333,
scheme: 'bitcoin',
sslScheme: null,
suffix: '',
},
},
},
currentDependencies: {
bitcoind: {
title: Mock.BitcoinDep.title,
icon: Mock.BitcoinDep.icon,
kind: 'running',
versionRange: '>=26.0.0',
healthChecks: [],
},
'btc-rpc-proxy': {
title: Mock.ProxyDep.title,
icon: Mock.ProxyDep.icon,
kind: 'running',
versionRange: '>2.0.0',
healthChecks: [],
},
},
hosts: {},
storeExposedDependents: [],
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
tasks: {
config: {
active: true,
task: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
task: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': {
active: true,
task: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason: 'LND likes BTC a certain way',
input: {
kind: 'partial',
value: {
color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
task: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: {
rpcuser: 'lnd',
},
},
},
},
},
},
},
},
}