outbound gateway support (#3120)

* Multiple (#3111)

* fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete

* trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed

* Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112)

* Fix PackageInfoShort to handle LocaleString on releaseNotes

* fix: filter by target_version in get_matching_models and pass otherVersions from install

* chore: add exver documentation for ai agents

* frontend plus some be types

---------

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Matt Hill
2026-02-12 08:27:09 -07:00
committed by GitHub
parent 2a54625f43
commit 8ef4ecf5ac
37 changed files with 1113 additions and 239 deletions

View File

@@ -0,0 +1,121 @@
import { Component } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
standalone: true,
imports: [TuiButton, i18nPipe],
template: `
<div class="animation-container">
<div class="port">
<div class="port-inner"></div>
</div>
<div class="usb-stick">
<div class="usb-connector"></div>
<div class="usb-body"></div>
</div>
</div>
<p>
{{
'Remove USB stick or other installation media from your server' | i18n
}}
</p>
<footer>
<button tuiButton (click)="context.completeWith(true)">
{{ 'Done' | i18n }}
</button>
</footer>
`,
styles: `
:host {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.animation-container {
position: relative;
width: 160px;
height: 69px;
}
.port {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 18px;
background: var(--tui-background-neutral-1);
border: 2px solid var(--tui-border-normal);
border-radius: 2px;
}
.port-inner {
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--tui-background-neutral-2);
border-radius: 1px;
}
.usb-stick {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
animation: slide-out 2s ease-in-out 0.5s infinite;
left: 32px;
}
.usb-connector {
width: 20px;
height: 12px;
background: var(--tui-text-secondary);
border-radius: 1px;
}
.usb-body {
width: 40px;
height: 20px;
background: var(--tui-status-info);
border-radius: 2px 4px 4px 2px;
}
@keyframes slide-out {
0% {
left: 32px;
opacity: 0;
}
5% {
left: 32px;
opacity: 1;
}
80% {
left: 130px;
opacity: 0;
}
100% {
left: 130px;
opacity: 0;
}
}
p {
margin: 0 0 2rem;
}
footer {
display: flex;
justify-content: center;
}
`,
})
export class RemoveMediaDialog {
protected readonly context = injectContext<TuiDialogContext<boolean>>()
}

View File

@@ -1,4 +1,9 @@
import { ChangeDetectorRef, Component, inject } from '@angular/core'
import {
ChangeDetectorRef,
Component,
HostListener,
inject,
} from '@angular/core'
import { Router } from '@angular/router'
import { FormsModule } from '@angular/forms'
import {
@@ -21,13 +26,14 @@ import {
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { filter } from 'rxjs'
import { filter, Subscription } from 'rxjs'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
@Component({
template: `
@if (!shuttingDown) {
<section tuiCardLarge="compact">
<header tuiHeader>
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
}
</footer>
</section>
}
`,
styles: `
.no-drives {
@@ -176,6 +183,14 @@ export default class DrivesPage {
protected readonly mobile = inject(TUI_IS_MOBILE)
@HostListener('document:keydown', ['$event'])
onKeydown(event: KeyboardEvent) {
if (event.ctrlKey && event.shiftKey && event.key === 'X') {
event.preventDefault()
this.shutdownServer()
}
}
readonly osDriveTooltip = this.i18n.transform(
'The drive where the StartOS operating system will be installed.',
)
@@ -185,6 +200,8 @@ export default class DrivesPage {
drives: DiskInfo[] = []
loading = true
shuttingDown = false
private dialogSub?: Subscription
selectedOsDrive: DiskInfo | null = null
selectedDataDrive: DiskInfo | null = null
preserveData: boolean | null = null
@@ -339,22 +356,18 @@ export default class DrivesPage {
loader.unsubscribe()
// Show success dialog
this.dialogs
.openConfirm({
this.dialogSub = this.dialogs
.openAlert('StartOS has been installed successfully.', {
label: 'Installation Complete!',
size: 's',
data: {
content: 'StartOS has been installed successfully.',
yes: 'Continue to Setup',
no: 'Shutdown',
},
dismissible: false,
closeable: true,
data: { button: this.i18n.transform('Continue to Setup') },
})
.subscribe(continueSetup => {
if (continueSetup) {
.subscribe({
complete: () => {
this.navigateToNextStep(result.attach)
} else {
this.shutdownServer()
}
},
})
} catch (e: any) {
loader.unsubscribe()
@@ -372,10 +385,12 @@ export default class DrivesPage {
}
private async shutdownServer() {
this.dialogSub?.unsubscribe()
const loader = this.loader.open('Beginning shutdown').subscribe()
try {
await this.api.shutdown()
this.shuttingDown = true
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -6,7 +6,12 @@ import {
ViewChild,
DOCUMENT,
} from '@angular/core'
import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared'
import {
DialogService,
DownloadHTMLService,
ErrorService,
i18nPipe,
} from '@start9labs/shared'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
@@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { DocumentationComponent } from '../components/documentation.component'
import { MatrixComponent } from '../components/matrix.component'
import { RemoveMediaDialog } from '../components/remove-media.dialog'
import { SetupCompleteRes } from '../types'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@Component({
template: `
@@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types'
@if (!stateService.kiosk) {
<span tuiSubtitle>
{{
stateService.setupType === 'restore'
? ('You can unplug your backup drive' | i18n)
: stateService.setupType === 'transfer'
? ('You can unplug your transfer drive' | i18n)
: ('http://start.local was for setup only. It will no longer work.'
| i18n)
'http://start.local was for setup only. It will no longer work.'
| i18n
}}
</span>
}
@@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types'
tuiCell="l"
[class.disabled]="!stateService.kiosk && !downloaded"
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
(click)="usbRemoved = true"
(click)="removeMedia()"
>
<tui-avatar appearance="secondary" src="@tui.usb" />
<div tuiTitle>
{{ 'USB Removed' | i18n }}
{{ 'Remove Installation Media' | i18n }}
<div tuiSubtitle>
{{
'Remove the USB installation media from your server' | i18n
'Remove USB stick or other installation media from your server'
| i18n
}}
</div>
</div>
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit {
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly downloadHtml = inject(DownloadHTMLService)
private readonly dialogs = inject(DialogService)
private readonly i18n = inject(i18nPipe)
readonly stateService = inject(StateService)
@@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit {
})
}
removeMedia() {
this.dialogs
.openComponent<boolean>(
new PolymorpheusComponent(RemoveMediaDialog),
{
size: 's',
dismissible: false,
closeable: false,
},
)
.subscribe(() => {
this.usbRemoved = true
})
}
exitKiosk() {
this.api.exit()
}