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

View File

@@ -24,16 +24,29 @@ export default {
general: {
title: 'Configuración General',
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',
language: 'Idioma',
tor: 'Reiniciar Tor',
daemon: 'Reiniciar el daemon de Tor en tu servidor',
disk: 'Reparación de Disco',
attempt: 'Intentar reparación automática',
repair: 'Reparar',
repair: {
title: 'Reparación de Disco',
subtitle: 'Intentar reparación automática',
button: '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: {
title: 'Fallo en la sincronización del reloj',
subtitle:

View File

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

View File

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

View File

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

View File

@@ -1,44 +1,87 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiSelectModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy'
import { KeyValuePipe } from '@angular/common'
import {
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 { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.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({
template: `
<ng-container *title>Logs</ng-container>
<tui-select
tuiTextfieldAppearance="secondary"
tuiTextfieldSize="m"
[style.max-width.rem]="26"
[(ngModel)]="logs"
>
{{ subtitle }}
<select tuiSelect [items]="items"></select>
</tui-select>
@switch (logs) {
@case ('OS Logs') {
<logs
context="OS Logs"
[followLogs]="followOS"
[fetchLogs]="fetchOS"
></logs>
<ng-container *title>
@if (current(); as key) {
<button
tuiIconButton
iconStart="@tui.arrow-left"
(click)="current.set(null)"
>
Back
</button>
{{ logs[key].title }}
} @else {
Logs
}
@case ('Kernel Logs') {
<logs
context="Kernel Logs"
[followLogs]="followKernel"
[fetchLogs]="fetchKernel"
></logs>
</ng-container>
@if (current(); as key) {
<header tuiTitle>
<strong class="title">
<button
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') {
<logs
context="Tor Logs"
[followLogs]="followTor"
[fetchLogs]="fetchTor"
></logs>
} @else {
@for (log of logs | keyvalue; track $index) {
<button
tuiCardMedium
tuiAppearance="neutral"
(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' },
styles: [
`
tui-select {
margin: 1rem 0;
:host {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
}
header {
width: 100%;
padding: 0 1rem;
}
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: [
FormsModule,
TuiSelectModule,
TuiTextfieldControllerModule,
LogsComponent,
TitleDirective,
KeyValuePipe,
TuiTitle,
TuiCardMedium,
TuiIcon,
TuiAppearance,
TuiLink,
TuiButton,
],
})
export default class SystemLogsComponent {
private readonly api = inject(ApiService)
readonly items = ['OS Logs', 'Kernel Logs', 'Tor Logs']
logs = 'OS Logs'
readonly followOS = async (params: RR.FollowServerLogsReq) =>
this.api.followServerLogs(params)
readonly fetchOS = async (params: RR.GetServerLogsReq) =>
this.api.getServerLogs(params)
readonly followKernel = async (params: RR.FollowServerLogsReq) =>
this.api.followKernelLogs(params)
readonly fetchKernel = async (params: RR.GetServerLogsReq) =>
this.api.getKernelLogs(params)
readonly followTor = async (params: RR.FollowServerLogsReq) =>
this.api.followTorLogs(params)
readonly fetchTor = async (params: RR.GetServerLogsReq) =>
this.api.getTorLogs(params)
get subtitle(): string {
switch (this.logs) {
case 'OS Logs':
return 'Raw, unfiltered operating system logs'
case 'Kernel Logs':
return 'Diagnostic log stream for device drivers and other kernel processes'
default:
return 'Diagnostic log stream for the Tor daemon on StartOS'
}
readonly current = signal<string | null>(null)
readonly logs: Record<string, Log> = {
os: {
title: 'OS Logs',
subtitle: 'Raw, unfiltered operating system logs',
icon: '@tui.square-dashed-bottom-code',
follow: params => this.api.followServerLogs(params),
fetch: params => this.api.getServerLogs(params),
},
kernel: {
title: 'Kernel Logs',
subtitle: 'Diagnostics for drivers and other kernel processes',
icon: '@tui.square-chevron-right',
follow: params => this.api.followKernelLogs(params),
fetch: params => this.api.getKernelLogs(params),
},
tor: {
title: 'Tor Logs',
subtitle: 'Diagnostic logs for the Tor daemon on StartOS',
icon: '@tui.globe',
follow: params => this.api.followTorLogs(params),
fetch: params => this.api.getTorLogs(params),
},
}
}

View File

@@ -8,7 +8,9 @@ import {
import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router'
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 { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
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>
{{ interface()?.name }}
</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) {
<app-interface
[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' },
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [InterfaceComponent, RouterLink, TuiButton, TitleDirective],
imports: [
InterfaceComponent,
RouterLink,
TuiButton,
TitleDirective,
TuiBreadcrumbs,
TuiItem,
TuiLink,
],
})
export default class ServiceInterfaceRoute {
private readonly config = inject(ConfigService)

View File

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

View File

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

View File

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

View File

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