diff --git a/web/projects/ui/src/app/i18n/dictionaries/english.ts b/web/projects/ui/src/app/i18n/dictionaries/english.ts
index e7331678a..0a21bbe44 100644
--- a/web/projects/ui/src/app/i18n/dictionaries/english.ts
+++ b/web/projects/ui/src/app/i18n/dictionaries/english.ts
@@ -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:
diff --git a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts b/web/projects/ui/src/app/i18n/dictionaries/spanish.ts
index 82c915987..d84118996 100644
--- a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts
+++ b/web/projects/ui/src/app/i18n/dictionaries/spanish.ts
@@ -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:
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts
index 12eba0df4..ce9981dde 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts
@@ -61,6 +61,15 @@ type ClearnetForm = {
Learn More
+
@if (clearnet().length) {
-
}
@if (clearnet().length) {
@@ -110,8 +111,10 @@ type ClearnetForm = {
} @else {
- No interfaces available
-
+ No public addresses
+
}
`,
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts
index 6687420a6..2f8451ee4 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts
@@ -54,6 +54,16 @@ type OnionForm = {
Learn More
+ @if (tor().length) {
+
+ }
@if (tor().length) {
@@ -85,7 +95,7 @@ type OnionForm = {
} @else {
No Tor addresses available
-
+
}
`,
@@ -149,7 +159,7 @@ export class InterfaceTorComponent {
async add() {
const options: Partial>> = {
- label: 'Select Domain/Subdomain',
+ label: 'New Tor Address',
data: {
spec: await configBuilderToSpec(
ISB.InputSpec.of({
diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss
index d2aef6bd6..928250f8b 100644
--- a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss
+++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.scss
@@ -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'] {
diff --git a/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts b/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts
index 6f54c95dc..fc5afd5d0 100644
--- a/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/logs/logs.component.ts
@@ -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
+ fetch: (params: RR.GetServerLogsReq) => Promise
+}
+
@Component({
template: `
- Logs
-
- {{ subtitle }}
-
-
- @switch (logs) {
- @case ('OS Logs') {
-
+
+ @if (current(); as key) {
+
+ {{ logs[key].title }}
+ } @else {
+ Logs
}
- @case ('Kernel Logs') {
-
+
+ @if (current(); as key) {
+
+
+
+ {{ logs[key].title }}
+
+ {{ logs[key].subtitle }}
+
+ @for (log of logs | keyvalue; track $index) {
+ @if (log.key === current()) {
+
+ }
}
- @case ('Tor Logs') {
-
+ } @else {
+ @for (log of logs | keyvalue; track $index) {
+
}
}
`,
@@ -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(null)
+ readonly logs: Record = {
+ 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),
+ },
}
}
diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts
index 876063fa8..464fc323e 100644
--- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts
@@ -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'
Back
{{ interface()?.name }}
+
+
+ Dashboard
+
+ {{ interface()?.name }}
+
@if (interface(); as serviceInterface) {
}
`,
+ 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)
diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts
index dc38446d6..816b785a0 100644
--- a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts
@@ -52,6 +52,9 @@ const ICONS = {
>
{{ item }}
+ @if (item === 'dashboard') {
+
+ }
}
@@ -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;
}
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
index bdc30d25f..f2d4196ea 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts
@@ -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'
- {{ 'system.general.update' | i18n }}
+ {{ 'system.general.update.title' | i18n }}
{{ server.version }}
@@ -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 }}
}
}
@@ -124,14 +124,28 @@ import { SystemWipeComponent } from './wipe.component'
/>
+
+
+
+
+ {{ 'system.general.ca.title' | i18n }}
+
+
+ {{ 'system.general.ca.subtitle' | i18n }}
+
+
+
+
- {{ 'system.general.tor' | i18n }}
+ {{ 'system.general.tor.title' | i18n }}
- {{ 'system.general.daemon' | i18n }}
+ {{ 'system.general.tor.subtitle' | i18n }}
}
}
+
+
`,
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, {
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts
index 31bb6727e..15e411cff 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts
@@ -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: `
Back
- Web Addresses
+ StartOS UI
@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)
diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss
index 62ebb31b9..85511c705 100644
--- a/web/projects/ui/src/styles.scss
+++ b/web/projects/ui/src/styles.scss
@@ -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;
}