feat: refactor logs (#2856)

* feat: refactor logs

* download root ca, minor transaltions, better interfaces buttons

* chore: comments

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Alex Inkin
2025-03-28 17:02:06 +04:00
committed by GitHub
parent e6af7e9885
commit 495bbecc01
11 changed files with 315 additions and 115 deletions

View File

@@ -12,7 +12,7 @@ export default {
email: 'Email', email: 'Email',
backup: 'Create Backup', backup: 'Create Backup',
restore: 'Restore Backup', restore: 'Restore Backup',
interfaces: 'User Interface Addresses', interfaces: 'StartOS UI',
acme: 'ACME', acme: 'ACME',
wifi: 'WiFi', wifi: 'WiFi',
sessions: 'Active Sessions', sessions: 'Active Sessions',
@@ -22,16 +22,29 @@ export default {
general: { general: {
title: 'General Settings', title: 'General Settings',
subtitle: 'Manage your overall setup and preferences', subtitle: 'Manage your overall setup and preferences',
update: 'Software Update',
restart: 'Restart to apply',
check: 'Check for updates',
tab: 'Browser Tab Title', tab: 'Browser Tab Title',
language: 'Language', language: 'Language',
tor: 'Reset Tor', repair: {
daemon: 'Restart the Tor daemon on your server', title: 'Disk Repair',
disk: 'Disk Repair', subtitle: 'Attempt automatic repair',
attempt: 'Attempt automatic repair', button: 'Repair',
repair: 'Repair', },
ca: {
title: 'Root Certificate Authority',
subtitle: `Download your server's Root CA`,
button: 'Download',
},
tor: {
title: 'Reset Tor',
subtitle: 'Restart the Tor daemon on your server',
},
update: {
title: 'Software Update',
button: {
restart: 'Restart to apply',
check: 'Check for updates',
},
},
sync: { sync: {
title: 'Clock sync failure', title: 'Clock sync failure',
subtitle: subtitle:

View File

@@ -24,16 +24,29 @@ export default {
general: { general: {
title: 'Configuración General', title: 'Configuración General',
subtitle: 'Gestiona tu configuración general y preferencias', subtitle: 'Gestiona tu configuración general y preferencias',
update: 'Actualización de Software',
restart: 'Reiniciar para aplicar',
check: 'Buscar actualizaciones',
tab: 'Título de la Pestaña del Navegador', tab: 'Título de la Pestaña del Navegador',
language: 'Idioma', language: 'Idioma',
tor: 'Reiniciar Tor', repair: {
daemon: 'Reiniciar el daemon de Tor en tu servidor', title: 'Reparación de Disco',
disk: 'Reparación de Disco', subtitle: 'Intentar reparación automática',
attempt: 'Intentar reparación automática', button: 'Reparar',
repair: 'Reparar', },
ca: {
title: 'Autoridad de Certificación Raíz',
subtitle: 'Descarga la autoridad certificadora raíz de tu servidor',
button: 'Descarga',
},
tor: {
title: 'Reiniciar Tor',
subtitle: 'Reiniciar el daemon de Tor en tu servidor',
},
update: {
title: 'Actualización de Software',
button: {
restart: 'Reiniciar para aplicar',
check: 'Buscar actualizaciones',
},
},
sync: { sync: {
title: 'Fallo en la sincronización del reloj', title: 'Fallo en la sincronización del reloj',
subtitle: subtitle:

View File

@@ -61,6 +61,15 @@ type ClearnetForm = {
Learn More Learn More
</a> </a>
</ng-template> </ng-template>
<button
tuiButton
appearance="accent"
[iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'"
[style.margin-inline-start]="'auto'"
(click)="toggle()"
>
Make {{ isPublic() ? 'private' : 'public' }}
</button>
@if (clearnet().length) { @if (clearnet().length) {
<button <button
tuiButton tuiButton
@@ -70,14 +79,6 @@ type ClearnetForm = {
> >
Add Add
</button> </button>
<button
tuiButton
appearance="accent"
[iconStart]="isPublic() ? '@tui.globe-lock' : '@tui.globe'"
(click)="toggle()"
>
Make {{ isPublic() ? 'private' : 'public' }}
</button>
} }
</header> </header>
@if (clearnet().length) { @if (clearnet().length) {
@@ -110,8 +111,10 @@ type ClearnetForm = {
</table> </table>
} @else { } @else {
<app-placeholder icon="@tui.app-window"> <app-placeholder icon="@tui.app-window">
No interfaces available No public addresses
<button tuiButton iconStart="@tui.plus" (click)="add()">Add</button> <button tuiButton iconStart="@tui.plus" (click)="add()">
Add Domain
</button>
</app-placeholder> </app-placeholder>
} }
`, `,

View File

@@ -54,6 +54,16 @@ type OnionForm = {
Learn More Learn More
</a> </a>
</ng-template> </ng-template>
@if (tor().length) {
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
Add
</button>
}
</header> </header>
@if (tor().length) { @if (tor().length) {
<table [appTable]="['Protocol', 'URL', '']"> <table [appTable]="['Protocol', 'URL', '']">
@@ -85,7 +95,7 @@ type OnionForm = {
} @else { } @else {
<app-placeholder icon="@tui.app-window"> <app-placeholder icon="@tui.app-window">
No Tor addresses available No Tor addresses available
<button tuiButton iconStart="@tui.plus">Add</button> <button tuiButton iconStart="@tui.plus" (click)="add()">Add</button>
</app-placeholder> </app-placeholder>
} }
`, `,
@@ -149,7 +159,7 @@ export class InterfaceTorComponent {
async add() { async add() {
const options: Partial<TuiDialogOptions<FormContext<OnionForm>>> = { const options: Partial<TuiDialogOptions<FormContext<OnionForm>>> = {
label: 'Select Domain/Subdomain', label: 'New Tor Address',
data: { data: {
spec: await configBuilderToSpec( spec: await configBuilderToSpec(
ISB.InputSpec.of({ ISB.InputSpec.of({

View File

@@ -7,6 +7,9 @@
.scrollbar { .scrollbar {
flex: 1; flex: 1;
background: var(--tui-background-neutral-1);
border-radius: var(--tui-radius-m);
border: 1rem solid transparent;
} }
.loading-dots { .loading-dots {
@@ -27,7 +30,6 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--tui-background-neutral-1);
} }
[data-status='reconnecting'] { [data-status='reconnecting'] {

View File

@@ -1,44 +1,87 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { KeyValuePipe } from '@angular/common'
import { FormsModule } from '@angular/forms' import {
import { TuiSelectModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy' ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import {
TuiAppearance,
TuiButton,
TuiIcon,
TuiLink,
TuiTitle,
} from '@taiga-ui/core'
import { TuiCardMedium } from '@taiga-ui/layout'
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component' import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
import { RR } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
interface Log {
title: string
subtitle: string
icon: string
follow: (params: RR.FollowServerLogsReq) => Promise<RR.FollowServerLogsRes>
fetch: (params: RR.GetServerLogsReq) => Promise<RR.GetServerLogsRes>
}
@Component({ @Component({
template: ` template: `
<ng-container *title>Logs</ng-container> <ng-container *title>
<tui-select @if (current(); as key) {
tuiTextfieldAppearance="secondary" <button
tuiTextfieldSize="m" tuiIconButton
[style.max-width.rem]="26" iconStart="@tui.arrow-left"
[(ngModel)]="logs" (click)="current.set(null)"
> >
{{ subtitle }} Back
<select tuiSelect [items]="items"></select> </button>
</tui-select> {{ logs[key].title }}
@switch (logs) { } @else {
@case ('OS Logs') { Logs
<logs
context="OS Logs"
[followLogs]="followOS"
[fetchLogs]="fetchOS"
></logs>
} }
@case ('Kernel Logs') { </ng-container>
<logs @if (current(); as key) {
context="Kernel Logs" <header tuiTitle>
[followLogs]="followKernel" <strong class="title">
[fetchLogs]="fetchKernel" <button
></logs> tuiIconButton
appearance="secondary-grayscale"
iconStart="@tui.x"
size="s"
class="close"
(click)="current.set(null)"
>
Close
</button>
{{ logs[key].title }}
</strong>
<p tuiSubtitle>{{ logs[key].subtitle }}</p>
</header>
@for (log of logs | keyvalue; track $index) {
@if (log.key === current()) {
<logs
[context]="log.value.title"
[followLogs]="log.value.follow"
[fetchLogs]="log.value.fetch"
/>
}
} }
@case ('Tor Logs') { } @else {
<logs @for (log of logs | keyvalue; track $index) {
context="Tor Logs" <button
[followLogs]="followTor" tuiCardMedium
[fetchLogs]="fetchTor" tuiAppearance="neutral"
></logs> (click)="current.set(log.key)"
>
<tui-icon [icon]="log.value.icon" />
<span tuiTitle>
<strong>{{ log.value.title }}</strong>
<span tuiSubtitle>{{ log.value.subtitle }}</span>
</span>
<tui-icon icon="@tui.chevron-right" />
</button>
} }
} }
`, `,
@@ -47,51 +90,114 @@ import { TitleDirective } from 'src/app/services/title.service'
host: { class: 'g-page' }, host: { class: 'g-page' },
styles: [ styles: [
` `
tui-select { :host {
margin: 1rem 0; display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
}
header {
width: 100%;
padding: 0 1rem;
} }
logs { logs {
height: calc(100% - 5rem); height: calc(100% - 4rem);
width: 100%;
}
.close {
position: absolute;
right: 0;
border-radius: 100%;
}
button::before {
margin: 0 -0.25rem 0 -0.375rem;
--tui-icon-size: 1.5rem;
}
[tuiCardMedium] {
height: 14rem;
width: 14rem;
cursor: pointer;
box-shadow:
inset 0 0 0 1px var(--tui-background-neutral-1),
var(--tui-shadow-small);
[tuiSubtitle] {
color: var(--tui-text-secondary);
}
tui-icon:last-child {
align-self: flex-end;
}
}
:host-context(tui-root._mobile) {
flex-direction: column;
justify-content: flex-start;
header {
padding: 0;
}
.title {
display: none;
}
logs {
height: calc(100% - 2rem);
}
[tuiCardMedium] {
width: 100%;
height: auto;
gap: 1rem;
}
} }
`, `,
], ],
imports: [ imports: [
FormsModule,
TuiSelectModule,
TuiTextfieldControllerModule,
LogsComponent, LogsComponent,
TitleDirective, TitleDirective,
KeyValuePipe,
TuiTitle,
TuiCardMedium,
TuiIcon,
TuiAppearance,
TuiLink,
TuiButton,
], ],
}) })
export default class SystemLogsComponent { export default class SystemLogsComponent {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
readonly items = ['OS Logs', 'Kernel Logs', 'Tor Logs']
logs = 'OS Logs'
readonly followOS = async (params: RR.FollowServerLogsReq) => readonly current = signal<string | null>(null)
this.api.followServerLogs(params) readonly logs: Record<string, Log> = {
readonly fetchOS = async (params: RR.GetServerLogsReq) => os: {
this.api.getServerLogs(params) title: 'OS Logs',
subtitle: 'Raw, unfiltered operating system logs',
readonly followKernel = async (params: RR.FollowServerLogsReq) => icon: '@tui.square-dashed-bottom-code',
this.api.followKernelLogs(params) follow: params => this.api.followServerLogs(params),
readonly fetchKernel = async (params: RR.GetServerLogsReq) => fetch: params => this.api.getServerLogs(params),
this.api.getKernelLogs(params) },
kernel: {
readonly followTor = async (params: RR.FollowServerLogsReq) => title: 'Kernel Logs',
this.api.followTorLogs(params) subtitle: 'Diagnostics for drivers and other kernel processes',
readonly fetchTor = async (params: RR.GetServerLogsReq) => icon: '@tui.square-chevron-right',
this.api.getTorLogs(params) follow: params => this.api.followKernelLogs(params),
fetch: params => this.api.getKernelLogs(params),
get subtitle(): string { },
switch (this.logs) { tor: {
case 'OS Logs': title: 'Tor Logs',
return 'Raw, unfiltered operating system logs' subtitle: 'Diagnostic logs for the Tor daemon on StartOS',
case 'Kernel Logs': icon: '@tui.globe',
return 'Diagnostic log stream for device drivers and other kernel processes' follow: params => this.api.followTorLogs(params),
default: fetch: params => this.api.getTorLogs(params),
return 'Diagnostic log stream for the Tor daemon on StartOS' },
}
} }
} }

View File

@@ -8,7 +8,9 @@ import {
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core' import { TuiItem } from '@taiga-ui/cdk'
import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TuiBreadcrumbs } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
@@ -22,6 +24,12 @@ import { TitleDirective } from 'src/app/services/title.service'
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a>
{{ interface()?.name }} {{ interface()?.name }}
</ng-container> </ng-container>
<tui-breadcrumbs size="l" [style.margin-block-end.rem]="1">
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
Dashboard
</a>
<span *tuiItem class="g-primary">{{ interface()?.name }}</span>
</tui-breadcrumbs>
@if (interface(); as serviceInterface) { @if (interface(); as serviceInterface) {
<app-interface <app-interface
[packageId]="pkgId" [packageId]="pkgId"
@@ -29,10 +37,23 @@ import { TitleDirective } from 'src/app/services/title.service'
/> />
} }
`, `,
styles: `
:host-context(tui-root._mobile) tui-breadcrumbs {
display: none;
}
`,
host: { class: 'g-subpage' }, host: { class: 'g-subpage' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [InterfaceComponent, RouterLink, TuiButton, TitleDirective], imports: [
InterfaceComponent,
RouterLink,
TuiButton,
TitleDirective,
TuiBreadcrumbs,
TuiItem,
TuiLink,
],
}) })
export default class ServiceInterfaceRoute { export default class ServiceInterfaceRoute {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)

View File

@@ -52,6 +52,9 @@ const ICONS = {
> >
<tui-icon [icon]="icons[item]" /> <tui-icon [icon]="icons[item]" />
<span tuiTitle>{{ item }}</span> <span tuiTitle>{{ item }}</span>
@if (item === 'dashboard') {
<a routerLink="interface" routerLinkActive="active"></a>
}
</a> </a>
} }
</nav> </nav>
@@ -80,7 +83,12 @@ const ICONS = {
margin: 0 -0.5rem; margin: 0 -0.5rem;
} }
.active { a a {
display: none;
}
.active,
a:has(.active) {
color: var(--tui-text-primary); color: var(--tui-text-primary);
[tuiTitle] { [tuiTitle] {
@@ -117,7 +125,8 @@ const ICONS = {
background: var(--tui-background-neutral-1); background: var(--tui-background-neutral-1);
box-shadow: inset 0 -1px var(--tui-background-neutral-1); box-shadow: inset 0 -1px var(--tui-background-neutral-1);
&.active { &.active,
&:has(.active) {
background: none; background: none;
box-shadow: none; box-shadow: none;
} }

View File

@@ -1,4 +1,4 @@
import { AsyncPipe } from '@angular/common' import { AsyncPipe, DOCUMENT } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@@ -68,7 +68,7 @@ import { SystemWipeComponent } from './wipe.component'
<tui-icon icon="@tui.zap" /> <tui-icon icon="@tui.zap" />
<span tuiTitle> <span tuiTitle>
<strong> <strong>
{{ 'system.general.update' | i18n }} {{ 'system.general.update.title' | i18n }}
</strong> </strong>
<span tuiSubtitle>{{ server.version }}</span> <span tuiSubtitle>{{ server.version }}</span>
</span> </span>
@@ -79,12 +79,12 @@ import { SystemWipeComponent } from './wipe.component'
(click)="onUpdate()" (click)="onUpdate()"
> >
@if (server.statusInfo.updated) { @if (server.statusInfo.updated) {
{{ 'system.general.restart' | i18n }} {{ 'system.general.update.button.restart' | i18n }}
} @else { } @else {
@if (eos.showUpdate$ | async) { @if (eos.showUpdate$ | async) {
{{ 'ui.update' | i18n }} {{ 'ui.update' | i18n }}
} @else { } @else {
{{ 'system.general.check' | i18n }} {{ 'system.general.update.button.check' | i18n }}
} }
} }
</button> </button>
@@ -124,14 +124,28 @@ import { SystemWipeComponent } from './wipe.component'
/> />
</button> </button>
</div> </div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.download" />
<span tuiTitle>
<strong>
{{ 'system.general.ca.title' | i18n }}
</strong>
<span tuiSubtitle>
{{ 'system.general.ca.subtitle' | i18n }}
</span>
</span>
<button tuiButton (click)="downloadCA()">
{{ 'system.general.ca.button' | i18n }}
</button>
</div>
<div tuiCell tuiAppearance="outline-grayscale"> <div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.circle-power" (click)="count = count + 1" /> <tui-icon icon="@tui.circle-power" (click)="count = count + 1" />
<span tuiTitle> <span tuiTitle>
<strong> <strong>
{{ 'system.general.tor' | i18n }} {{ 'system.general.tor.title' | i18n }}
</strong> </strong>
<span tuiSubtitle> <span tuiSubtitle>
{{ 'system.general.daemon' | i18n }} {{ 'system.general.tor.subtitle' | i18n }}
</span> </span>
</span> </span>
<button tuiButton appearance="glass" (click)="onReset()"> <button tuiButton appearance="glass" (click)="onReset()">
@@ -143,18 +157,20 @@ import { SystemWipeComponent } from './wipe.component'
<tui-icon icon="@tui.briefcase-medical" /> <tui-icon icon="@tui.briefcase-medical" />
<span tuiTitle> <span tuiTitle>
<strong> <strong>
{{ 'system.general.disk' | i18n }} {{ 'system.general.repair.title' | i18n }}
</strong> </strong>
<span tuiSubtitle> <span tuiSubtitle>
{{ 'system.general.attempt' | i18n }} {{ 'system.general.repair.subtitle' | i18n }}
</span> </span>
</span> </span>
<button tuiButton appearance="glass" (click)="onRepair()"> <button tuiButton appearance="glass" (click)="onRepair()">
{{ 'system.general.repair' | i18n }} {{ 'system.general.repair.button' | i18n }}
</button> </button>
</div> </div>
} }
} }
<!-- hidden element for downloading cert -->
<a id="download-ca" href="/static/local-root-ca.crt"></a>
`, `,
styles: ` styles: `
:host { :host {
@@ -209,6 +225,7 @@ export default class SystemGeneralComponent {
SystemWipeComponent, SystemWipeComponent,
inject(INJECTOR), inject(INJECTOR),
) )
private readonly document = inject(DOCUMENT)
wipe = false wipe = false
count = 0 count = 0
@@ -268,6 +285,10 @@ export default class SystemGeneralComponent {
.subscribe(() => this.resetTor(this.wipe)) .subscribe(() => this.resetTor(this.wipe))
} }
downloadCA() {
this.document.getElementById('download-ca')?.click()
}
async onRepair() { async onRepair() {
this.dialogs this.dialogs
.open(TUI_CONFIRM, { .open(TUI_CONFIRM, {

View File

@@ -14,9 +14,9 @@ import { TitleDirective } from 'src/app/services/title.service'
const iface: T.ServiceInterface = { const iface: T.ServiceInterface = {
id: '', id: '',
name: 'StartOS User Interface', name: 'StartOS UI',
description: description:
'The primary user interface for your StartOS server, accessible from any browser.', 'The web user interface for your StartOS server, accessible from any browser.',
type: 'ui' as const, type: 'ui' as const,
masked: false, masked: false,
addressInfo: { addressInfo: {
@@ -33,15 +33,12 @@ const iface: T.ServiceInterface = {
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
Web Addresses StartOS UI
</ng-container> </ng-container>
<header tuiHeader> <header tuiHeader>
<hgroup tuiTitle> <hgroup tuiTitle>
<h3>User Interface Addresses</h3> <h3>{{ iface.name }}</h3>
<p tuiSubtitle> <p tuiSubtitle>{{ iface.description }}</p>
View and manage private and public addresses for accessing your
StartOS UI
</p>
</hgroup> </hgroup>
</header> </header>
@if (ui(); as ui) { @if (ui(); as ui) {
@@ -61,6 +58,7 @@ const iface: T.ServiceInterface = {
}) })
export default class StartOsUiComponent { export default class StartOsUiComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
iface = iface
readonly ui = toSignal( readonly ui = toSignal(
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(PatchDB)

View File

@@ -366,6 +366,10 @@ button.g-action {
color: var(--tui-status-info) !important; color: var(--tui-status-info) !important;
} }
.g-primary {
color: var(--tui-text-primary) !important;
}
.g-secondary { .g-secondary {
color: var(--tui-text-secondary) !important; color: var(--tui-text-secondary) !important;
} }