multiple bugs and better port forward ux

This commit is contained in:
Matt Hill
2026-03-03 19:04:20 -07:00
parent 16a2fe4e08
commit e999d89bbc
18 changed files with 290 additions and 118 deletions

View File

@@ -701,4 +701,7 @@ export default {
771: 'Spiel vorbei',
772: 'Beliebige Taste drücken oder tippen zum Starten',
773: 'Beliebige Taste drücken oder tippen zum Neustarten',
774: 'Der Portstatus kann nicht ermittelt werden, solange der Dienst nicht läuft',
775: 'Diese Adresse funktioniert nicht aus Ihrem lokalen Netzwerk aufgrund einer Router-Hairpinning-Einschränkung',
776: 'Aktion nicht gefunden',
} satisfies i18n

View File

@@ -701,4 +701,7 @@ export const ENGLISH: Record<string, number> = {
'Game Over': 771,
'Press any key or tap to start': 772,
'Press any key or tap to play again': 773,
'Port status cannot be determined while service is not running': 774,
'This address will not work from your local network due to a router hairpinning limitation': 775,
'Action not found': 776,
}

View File

@@ -701,4 +701,7 @@ export default {
771: 'Fin del juego',
772: 'Pulsa cualquier tecla o toca para empezar',
773: 'Pulsa cualquier tecla o toca para jugar de nuevo',
774: 'El estado del puerto no se puede determinar mientras el servicio no está en ejecución',
775: 'Esta dirección no funcionará desde tu red local debido a una limitación de hairpinning del router',
776: 'Acción no encontrada',
} satisfies i18n

View File

@@ -701,4 +701,7 @@ export default {
771: 'Partie terminée',
772: "Appuyez sur une touche ou touchez l'écran pour commencer",
773: "Appuyez sur une touche ou touchez l'écran pour rejouer",
774: "L'état du port ne peut pas être déterminé tant que le service n'est pas en cours d'exécution",
775: "Cette adresse ne fonctionnera pas depuis votre réseau local en raison d'une limitation de hairpinning du routeur",
776: 'Action introuvable',
} satisfies i18n

View File

@@ -701,4 +701,7 @@ export default {
771: 'Koniec gry',
772: 'Naciśnij dowolny klawisz lub dotknij, aby rozpocząć',
773: 'Naciśnij dowolny klawisz lub dotknij, aby zagrać ponownie',
774: 'Status portu nie może być określony, gdy usługa nie jest uruchomiona',
775: 'Ten adres nie będzie działać z Twojej sieci lokalnej z powodu ograniczenia hairpinning routera',
776: 'Nie znaleziono akcji',
} satisfies i18n

View File

@@ -287,9 +287,12 @@ export class AddressActionsComponent {
}
showDnsValidation() {
const port = this.address().hostnameInfo.port
if (port === null) return
this.domainHealth.showPublicDomainSetup(
this.address().hostnameInfo.hostname,
this.gatewayId(),
port,
)
}

View File

@@ -171,14 +171,18 @@ export class InterfaceAddressesComponent {
default: null,
patterns: [utils.Patterns.domain],
}).map(f => f.toLocaleLowerCase()),
authority: ISB.Value.select({
name: this.i18n.transform('Certificate Authority'),
description: this.i18n.transform(
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
),
values: authorities,
default: Object.keys(network.acme)[0] || 'local',
}),
...(iface.addSsl
? {
authority: ISB.Value.select({
name: this.i18n.transform('Certificate Authority'),
description: this.i18n.transform(
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
),
values: authorities,
default: Object.keys(network.acme)[0] || 'local',
}),
}
: {}),
})
this.formDialog.open(FormComponent, {
@@ -250,7 +254,13 @@ export class InterfaceAddressesComponent {
await this.api.osUiAddPublicDomain(params)
}
await this.domainHealth.checkPublicDomain(fqdn, gatewayId)
const port = this.gatewayGroup().addresses.find(
a => a.access === 'public' && a.hostnameInfo.port !== null,
)?.hostnameInfo.port
if (port !== undefined && port !== null) {
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, port)
}
return true
} catch (e: any) {

View File

@@ -14,6 +14,8 @@ import {
tuiSwitchOptionsProvider,
} from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { T } from '@start9labs/start-sdk'
@@ -28,7 +30,7 @@ export type DomainValidationData = {
fqdn: string
gateway: DnsGateway
port: number
initialResults?: { dnsPass: boolean; portPass: boolean }
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null }
}
@Component({
@@ -93,18 +95,12 @@ export type DomainValidationData = {
{{ 'create this port forwarding rule' | i18n }}
</p>
@let portRes = portResult();
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
<tr>
<td class="status">
@if (portLoading()) {
<tui-loader size="s" />
} @else if (portPass() === true) {
<tui-icon class="g-positive" icon="@tui.check" />
} @else if (portPass() === false) {
<tui-icon class="g-negative" icon="@tui.x" />
} @else {
<tui-icon class="g-secondary" icon="@tui.minus" />
}
<port-check-icon [result]="portRes" [loading]="portLoading()" />
</td>
<td>{{ context.data.port }}</td>
<td>{{ context.data.port }}</td>
@@ -121,6 +117,8 @@ export type DomainValidationData = {
</tr>
</table>
<port-check-warnings [result]="portRes" />
@if (!isManualMode) {
<footer class="g-buttons padding-top">
<button
@@ -217,6 +215,8 @@ export type DomainValidationData = {
TuiButtonLoading,
TuiIcon,
TuiLoader,
PortCheckIconComponent,
PortCheckWarningsComponent,
],
})
export class DomainValidationComponent {
@@ -234,11 +234,16 @@ export class DomainValidationComponent {
readonly dnsLoading = signal(false)
readonly portLoading = signal(false)
readonly dnsPass = signal<boolean | undefined>(undefined)
readonly portPass = signal<boolean | undefined>(undefined)
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
readonly allPass = computed(
() => this.dnsPass() === true && this.portPass() === true,
)
readonly allPass = computed(() => {
const result = this.portResult()
return (
this.dnsPass() === true &&
!!result?.openInternally &&
!!result?.openExternally
)
})
readonly isManualMode = !this.context.data.initialResults
@@ -246,7 +251,7 @@ export class DomainValidationComponent {
const initial = this.context.data.initialResults
if (initial) {
this.dnsPass.set(initial.dnsPass)
this.portPass.set(initial.portPass)
if (initial.portResult) this.portResult.set(initial.portResult)
}
}
@@ -275,7 +280,7 @@ export class DomainValidationComponent {
port: this.context.data.port,
})
this.portPass.set(result.reachable)
this.portResult.set(result)
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -1,5 +1,6 @@
import { inject, Injectable } from '@angular/core'
import { DialogService, ErrorService } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -15,28 +16,36 @@ export class DomainHealthService {
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
async checkPublicDomain(fqdn: string, gatewayId: string): Promise<void> {
async checkPublicDomain(
fqdn: string,
gatewayId: string,
port: number,
): Promise<void> {
try {
const gateway = await this.getGatewayData(gatewayId)
if (!gateway) return
const [dnsPass, portPass] = await Promise.all([
const [dnsPass, portResult] = await Promise.all([
this.api
.queryDns({ fqdn })
.then(ip => ip === gateway.ipInfo.wanIp)
.catch(() => false),
this.api
.checkPort({ gateway: gatewayId, port: 443 })
.then(r => r.reachable)
.catch(() => false),
.checkPort({ gateway: gatewayId, port })
.catch((): null => null),
])
if (!dnsPass || !portPass) {
const portOk =
!!portResult?.openInternally &&
!!portResult?.openExternally &&
!!portResult?.hairpinning
if (!dnsPass || !portOk) {
setTimeout(
() =>
this.openPublicDomainModal(fqdn, gateway, 443, {
this.openPublicDomainModal(fqdn, gateway, port, {
dnsPass,
portPass,
portResult,
}),
250,
)
@@ -66,12 +75,16 @@ export class DomainHealthService {
}
}
async showPublicDomainSetup(fqdn: string, gatewayId: string): Promise<void> {
async showPublicDomainSetup(
fqdn: string,
gatewayId: string,
port: number,
): Promise<void> {
try {
const gateway = await this.getGatewayData(gatewayId)
if (!gateway) return
this.openPublicDomainModal(fqdn, gateway, 443)
this.openPublicDomainModal(fqdn, gateway, port)
} catch (e: any) {
this.errorService.handleError(e)
}
@@ -82,14 +95,18 @@ export class DomainHealthService {
const gateway = await this.getGatewayData(gatewayId)
if (!gateway) return
const portPass = await this.api
const portResult = await this.api
.checkPort({ gateway: gatewayId, port })
.then(r => r.reachable)
.catch(() => false)
.catch((): null => null)
if (!portPass) {
const portOk =
!!portResult?.openInternally &&
!!portResult?.openExternally &&
!!portResult?.hairpinning
if (!portOk) {
setTimeout(
() => this.openPortForwardModal(gateway, port, { portPass }),
() => this.openPortForwardModal(gateway, port, { portResult }),
250,
)
}
@@ -133,7 +150,7 @@ export class DomainHealthService {
fqdn: string,
gateway: DnsGateway,
port: number,
initialResults?: { dnsPass: boolean; portPass: boolean },
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null },
) {
this.dialog
.openComponent(DOMAIN_VALIDATION, {
@@ -147,7 +164,7 @@ export class DomainHealthService {
private openPortForwardModal(
gateway: DnsGateway,
port: number,
initialResults?: { portPass: boolean },
initialResults?: { portResult: T.CheckPortRes | null },
) {
this.dialog
.openComponent(PORT_FORWARD_VALIDATION, {

View File

@@ -29,7 +29,9 @@ import { DomainHealthService } from './domain-health.service'
tuiSwitch
size="s"
[showIcons]="false"
[disabled]="toggling() || address.hostnameInfo.metadata.kind === 'mdns'"
[disabled]="
toggling() || address.hostnameInfo.metadata.kind === 'mdns'
"
[ngModel]="address.enabled"
(ngModelChange)="onToggleEnabled()"
/>
@@ -207,10 +209,11 @@ export class InterfaceAddressItemComponent {
if (enabled) {
const kind = addr.hostnameInfo.metadata.kind
if (kind === 'public-domain') {
if (kind === 'public-domain' && addr.hostnameInfo.port !== null) {
await this.domainHealth.checkPublicDomain(
addr.hostnameInfo.hostname,
this.gatewayId(),
addr.hostnameInfo.port,
)
} else if (kind === 'private-domain') {
await this.domainHealth.checkPrivateDomain(this.gatewayId())

View File

@@ -1,13 +1,17 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
} from '@angular/core'
import { ErrorService, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { TuiButtonLoading } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DnsGateway } from './dns.component'
@@ -15,7 +19,7 @@ import { DnsGateway } from './dns.component'
export type PortForwardValidationData = {
gateway: DnsGateway
port: number
initialResults?: { portPass: boolean }
initialResults?: { portResult: T.CheckPortRes | null }
}
@Component({
@@ -30,18 +34,12 @@ export type PortForwardValidationData = {
{{ 'create this port forwarding rule' | i18n }}
</p>
@let portRes = portResult();
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
<tr>
<td class="status">
@if (loading()) {
<tui-loader size="s" />
} @else if (pass() === true) {
<tui-icon class="g-positive" icon="@tui.check" />
} @else if (pass() === false) {
<tui-icon class="g-negative" icon="@tui.x" />
} @else {
<tui-icon class="g-secondary" icon="@tui.minus" />
}
<port-check-icon [result]="portRes" [loading]="loading()" />
</td>
<td>{{ context.data.port }}</td>
<td>{{ context.data.port }}</td>
@@ -53,19 +51,21 @@ export type PortForwardValidationData = {
</tr>
</table>
<port-check-warnings [result]="portRes" />
@if (!isManualMode) {
<footer class="g-buttons padding-top">
<button
tuiButton
appearance="flat"
[disabled]="pass() === true"
[disabled]="portOk()"
(click)="context.completeWith()"
>
{{ 'Later' | i18n }}
</button>
<button
tuiButton
[disabled]="pass() !== true"
[disabled]="!portOk()"
(click)="context.completeWith()"
>
{{ 'Done' | i18n }}
@@ -132,8 +132,8 @@ export type PortForwardValidationData = {
i18nPipe,
TableComponent,
TuiButtonLoading,
TuiIcon,
TuiLoader,
PortCheckIconComponent,
PortCheckWarningsComponent,
],
})
export class PortForwardValidationComponent {
@@ -144,14 +144,23 @@ export class PortForwardValidationComponent {
injectContext<TuiDialogContext<void, PortForwardValidationData>>()
readonly loading = signal(false)
readonly pass = signal<boolean | undefined>(undefined)
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
readonly portOk = computed(() => {
const result = this.portResult()
return (
!!result?.openInternally &&
!!result?.openExternally &&
!!result?.hairpinning
)
})
readonly isManualMode = !this.context.data.initialResults
constructor() {
const initial = this.context.data.initialResults
if (initial) {
this.pass.set(initial.portPass)
if (initial.portResult) this.portResult.set(initial.portResult)
}
}
@@ -164,7 +173,7 @@ export class PortForwardValidationComponent {
port: this.context.data.port,
})
this.pass.set(result.reachable)
this.portResult.set(result)
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
@Component({
selector: 'port-check-icon',
template: `
@if (loading()) {
<tui-loader size="s" />
} @else {
@let res = result();
@if (res) {
@if (!res.openInternally) {
<tui-icon class="g-warning" icon="@tui.alert-triangle" />
} @else if (!res.openExternally) {
<tui-icon class="g-negative" icon="@tui.x" />
} @else {
<tui-icon class="g-positive" icon="@tui.check" />
}
} @else {
<tui-icon class="g-secondary" icon="@tui.minus" />
}
}
`,
styles: `
tui-icon {
font-size: 1.3rem;
vertical-align: text-bottom;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, TuiLoader],
})
export class PortCheckIconComponent {
readonly result = input<T.CheckPortRes>()
readonly loading = input(false)
}

View File

@@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
@Component({
selector: 'port-check-warnings',
template: `
@let res = result();
@if (res) {
@if (!res.openInternally) {
<p class="g-warning">
{{
'Port status cannot be determined while service is not running'
| i18n
}}
</p>
}
@if (res.openExternally && !res.hairpinning) {
<p class="g-warning">
{{
'This address will not work from your local network due to a router hairpinning limitation'
| i18n
}}
</p>
}
}
`,
styles: `
p {
margin-top: 0.5rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [i18nPipe],
})
export class PortCheckWarningsComponent {
readonly result = input<T.CheckPortRes>()
}

View File

@@ -19,7 +19,12 @@ import { ServiceTasksComponent } from 'src/app/routes/portal/routes/services/com
import { ActionService } from 'src/app/services/action.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getInstalledBaseStatus } from 'src/app/services/pkg-status-rendering.service'
import {
ALLOWED_STATUSES,
getInstalledBaseStatus,
INACTIVE_STATUSES,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data'
@Component({
@@ -49,6 +54,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
</td>
<td class="g-secondary" [style.grid-row]="3">
{{ task().reason || ('No reason provided' | i18n) }}
@if (disabled()) {
<div class="g-warning">{{ disabled() }}</div>
}
</td>
<td>
@if (task().severity !== 'critical') {
@@ -66,7 +74,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
tuiIconButton
iconStart="@tui.play"
appearance="primary-success"
[disabled]="!pkg()"
[disabled]="!!disabled()"
(click)="handle()"
>
{{ 'Run' | i18n }}
@@ -113,7 +121,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
}
}
`,
host: { '[style.opacity]': 'pkg() ? null : "var(--tui-disabled-opacity)"' },
host: {
'[style.opacity]': '!disabled() ? null : "var(--tui-disabled-opacity)"',
},
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiAvatar, i18nPipe, TuiFade],
})
@@ -124,6 +134,7 @@ export class ServiceTaskComponent {
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly tasks = inject(ServiceTasksComponent)
private readonly i18n = inject(i18nPipe)
readonly task = input.required<T.Task & { replayId: string }>()
readonly services = input.required<Record<string, PackageDataEntry>>()
@@ -135,6 +146,28 @@ export class ServiceTaskComponent {
() => this.tasks.pkg().currentDependencies[this.task().packageId],
)
readonly disabled = computed(() => {
const pkg = this.pkg()
if (!pkg) return this.i18n.transform('Not installed')!
const action = pkg.actions[this.task().actionId]
if (!action) return this.i18n.transform('Action not found')!
const status = renderPkgStatus(pkg).primary
if (INACTIVE_STATUSES.includes(status)) return status as string
if (!ALLOWED_STATUSES[action.allowedStatuses].has(status)) {
return `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`
}
if (typeof action.visibility === 'object') {
return action.visibility.disabled
}
return false
})
async dismiss() {
const { packageId, replayId } = this.task()

View File

@@ -23,6 +23,8 @@ import { StandardActionsService } from 'src/app/services/standard-actions.servic
import { getManifest } from 'src/app/utils/get-package-data'
import { ServiceActionComponent } from '../components/action.component'
import {
ALLOWED_STATUSES,
INACTIVE_STATUSES,
PrimaryStatus,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
@@ -30,30 +32,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
const INACTIVE: PrimaryStatus[] = [
'installing',
'updating',
'removing',
'restoring',
'backing-up',
'error',
]
const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
'only-running': new Set(['running']),
'only-stopped': new Set(['stopped']),
any: new Set([
'running',
'stopped',
'restarting',
'restoring',
'stopping',
'starting',
'backing-up',
'task-required',
]),
}
@Component({
template: `
@if (package(); as pkg) {
@@ -286,6 +264,6 @@ export default class ServiceActionsRoute {
}
protected readonly isInactive = computed(
(pkg = this.package()) => !pkg || INACTIVE.includes(pkg.status),
(pkg = this.package()) => !pkg || INACTIVE_STATUSES.includes(pkg.status),
)
}

View File

@@ -6,12 +6,15 @@ import {
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ErrorService, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { TuiButtonLoading } from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map } from 'rxjs'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -58,17 +61,13 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
@for (iface of row.interfaces; track iface) {
<div>{{ iface }}</div>
}
<port-check-warnings [result]="results()[i]" />
</td>
<td class="status">
@if (loading()[i]) {
<tui-loader size="s" />
} @else if (results()[i] === true) {
<tui-icon class="g-positive" icon="@tui.check" />
} @else if (results()[i] === false) {
<tui-icon class="g-negative" icon="@tui.x" />
} @else {
<tui-icon class="g-secondary" icon="@tui.minus" />
}
<port-check-icon
[result]="results()[i]"
[loading]="!!loading()[i]"
/>
</td>
<td>{{ row.externalPort }}</td>
<td>{{ row.internalPort }}</td>
@@ -103,11 +102,6 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
white-space: nowrap;
}
tui-icon {
font-size: 1.3rem;
vertical-align: text-bottom;
}
.status {
width: 3.2rem;
}
@@ -145,8 +139,8 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
i18nPipe,
TableComponent,
PlaceholderComponent,
TuiIcon,
TuiLoader,
PortCheckIconComponent,
PortCheckWarningsComponent,
TuiButtonLoading,
],
})
@@ -159,7 +153,7 @@ export class PortForwardsModalComponent {
injectContext<TuiDialogContext<void, PortForwardsModalData>>()
readonly loading = signal<Record<number, boolean>>({})
readonly results = signal<Record<number, boolean>>({})
readonly results = signal<Record<number, T.CheckPortRes>>({})
private readonly portForwards$ = combineLatest([
this.patch.watch$('serverInfo', 'network', 'host', 'portForwards').pipe(
@@ -254,7 +248,7 @@ export class PortForwardsModalComponent {
port,
})
this.results.update(r => ({ ...r, [index]: result.reachable }))
this.results.update(r => ({ ...r, [index]: result }))
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core'
import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace'
import {
FullKeyboard,
pauseFor,
RPCErrorDetails,
SetLanguageParams,
} from '@start9labs/shared'
import { ApiService } from './embassy-api.service'
import { T } from '@start9labs/start-sdk'
import {
AddOperation,
Dump,
@@ -16,6 +17,8 @@ import {
ReplaceOperation,
Revision,
} from 'patch-db-client'
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
import { WebSocketSubject } from 'rxjs/webSocket'
import {
DataModel,
InstallingState,
@@ -23,7 +26,9 @@ import {
StateInfo,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace'
import { toAuthorityUrl } from 'src/app/utils/acme'
import { AuthService } from '../auth.service'
import { Mock } from './api.fixures'
import {
ActionRes,
CheckDnsRes,
@@ -44,13 +49,8 @@ import {
ServerState,
WebsocketConfig,
} from './api.types'
import { Mock } from './api.fixures'
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
import { ApiService } from './embassy-api.service'
import { mockPatchData } from './mock-patch'
import { AuthService } from '../auth.service'
import { T } from '@start9labs/start-sdk'
import { WebSocketSubject } from 'rxjs/webSocket'
import { toAuthorityUrl } from 'src/app/utils/acme'
import markdown from './md-sample.md'
@@ -521,7 +521,13 @@ export class MockApiService extends ApiService {
async checkPort(params: T.CheckPortParams): Promise<T.CheckPortRes> {
await pauseFor(2000)
return { ip: '0.0.0.0', port: params.port, reachable: false }
return {
ip: '0.0.0.0',
port: params.port,
openExternally: true,
openInternally: false,
hairpinning: true,
}
}
async checkDns(params: T.CheckDnsParams): Promise<CheckDnsRes> {

View File

@@ -2,6 +2,30 @@ import { i18nKey } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
export const INACTIVE_STATUSES: PrimaryStatus[] = [
'installing',
'updating',
'removing',
'restoring',
'backing-up',
'error',
]
export const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
'only-running': new Set(['running']),
'only-stopped': new Set(['stopped']),
any: new Set([
'running',
'stopped',
'restarting',
'restoring',
'stopping',
'starting',
'backing-up',
'task-required',
]),
}
export interface PackageStatus {
primary: PrimaryStatus
health: T.HealthStatus | null