From deb0b1e5612ec633c7f1dbbfe98cbfdb02ed4754 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 4 Mar 2021 23:57:02 -0700 Subject: [PATCH] rework LAN display and service launchability --- ui/src/app/models/app-types.ts | 9 ++- .../app-installed-list.page.html | 14 +++-- .../app-installed-list.page.ts | 9 +-- .../app-installed-show.page.html | 7 ++- .../app-installed-show.page.ts | 18 +++--- .../app-metrics/app-metrics.page.ts | 1 - .../app/pages/server-routes/lan/lan.page.ts | 6 +- ui/src/app/services/api/api-types.ts | 6 +- .../app/services/api/api.service.factory.ts | 4 +- ui/src/app/services/api/api.service.ts | 4 +- ui/src/app/services/api/live-api.service.ts | 34 ++++++++++-- ui/src/app/services/api/mock-api.service.ts | 55 ++++++++++++++++--- ui/src/app/services/api/mock-app-fixures.ts | 30 +++++----- ui/src/app/services/config.service.ts | 19 ++++++- 14 files changed, 152 insertions(+), 64 deletions(-) diff --git a/ui/src/app/models/app-types.ts b/ui/src/app/models/app-types.ts index bac656409..b1da39aff 100644 --- a/ui/src/app/models/app-types.ts +++ b/ui/src/app/models/app-types.ts @@ -37,18 +37,23 @@ export interface AppInstalledPreview extends BaseApp { lanAddress?: string torAddress: string versionInstalled: string - ui: boolean + lanUi: boolean + torUi: boolean + // FE state only + hasUI: boolean + launchable: boolean } export interface AppInstalledFull extends AppInstalledPreview { instructions: string | null lastBackup: string | null configuredRequirements: AppDependency[] | null // null if not yet configured - hasFetchedFull: boolean startAlert?: string uninstallAlert?: string restoreAlert?: string actions: Actions + // FE state only + hasFetchedFull: boolean } export type Actions = ServiceAction[] diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html index 9f18c5c3d..0151a4f39 100644 --- a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html @@ -22,11 +22,17 @@ - + - -
-
+ +
+
diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts index 95c26cbb8..814713f09 100644 --- a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts @@ -35,19 +35,15 @@ export class AppInstalledListPage extends Cleanup { segmentValue: 'services' | 'embassy' = 'services' showCertDownload : boolean - isConsulate: boolean - isTor: boolean constructor ( private readonly serverModel: ServerModel, private readonly appModel: AppModel, private readonly preload: ModelPreload, private readonly syncDaemon: SyncDaemon, - config: ConfigService, + private readonly config: ConfigService, ) { super() - this.isConsulate = config.isConsulateAndroid || config.isConsulateIos - this.isTor = config.isTor() } ngOnDestroy () { @@ -105,12 +101,11 @@ export class AppInstalledListPage extends Cleanup { const app = this.apps.find(app => app.id === id).subject let uiAddress: string - if (this.isTor) { + if (this.config.isTor()) { uiAddress = `http://${app.torAddress.getValue()}` } else { uiAddress = `https://${app.lanAddress.getValue()}` } - console.log(uiAddress) return window.open(uiAddress, '_blank') } diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html index 202e4fcc0..09563fe50 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html @@ -20,7 +20,8 @@ hasFetchedFull: app.hasFetchedFull | async, iconURL: app.iconURL | async, title: app.title | async, - ui: app.ui | async, + hasUI: app.hasUI | async, + launchable: app.launchable | async, lanAddress: app.lanAddress | async } as vars" class="ion-padding-bottom"> @@ -77,7 +78,7 @@ - + Launch Web Interface @@ -96,7 +97,7 @@ - +

LAN Address

{{ vars.lanAddress }}

diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts index 25e8c7e0d..20565233c 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts @@ -32,8 +32,8 @@ export class AppInstalledShowPage extends Cleanup { appId: string AppStatus = AppStatus showInstructions = false - isConsulate: boolean - isTor: boolean + + hideLAN: boolean dependencyDefintion = () => `Dependencies are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}` @@ -51,11 +51,9 @@ export class AppInstalledShowPage extends Cleanup { private readonly wizardBaker: WizardBaker, private readonly appModel: AppModel, private readonly popoverController: PopoverController, - config: ConfigService, + private readonly config: ConfigService, ) { super() - this.isConsulate = config.isConsulateIos || config.isConsulateAndroid - this.isTor = config.isTor() } async ngOnInit () { @@ -64,8 +62,12 @@ export class AppInstalledShowPage extends Cleanup { this.cleanup( markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)) .pipe( - tap(app => this.app = app), - concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack + tap(app => { + this.app = app + const appP = peekProperties(this.app) + this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0 + }), + concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack catchError(e => of(this.setError(e))), ).subscribe(), ) @@ -98,7 +100,7 @@ export class AppInstalledShowPage extends Cleanup { async launchUiTab () { let uiAddress: string - if (this.isTor) { + if (this.config.isTor()) { uiAddress = `http://${this.app.torAddress.getValue()}` } else { uiAddress = `https://${this.app.lanAddress.getValue()}` diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts index 86cfb7137..160a7f18f 100644 --- a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -130,7 +130,6 @@ export class AppMetricsPage { toggleMask (key: string) { this.unmasked[key] = !this.unmasked[key] - console.log(this.unmasked) } asIsOrder (a: any, b: any) { diff --git a/ui/src/app/pages/server-routes/lan/lan.page.ts b/ui/src/app/pages/server-routes/lan/lan.page.ts index d098a201a..63cc149b2 100644 --- a/ui/src/app/pages/server-routes/lan/lan.page.ts +++ b/ui/src/app/pages/server-routes/lan/lan.page.ts @@ -16,10 +16,8 @@ export class LANPage { lanDocs = 'docs.start9labs.com/user-manual/general/secure-lan' lanAddress: string - isTor: boolean fullDocumentationLink: string - isConsulate: boolean - lanDisabled: LanSetupIssue = undefined + lanDisabled: LanSetupIssue readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = { NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`, NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`, @@ -40,8 +38,6 @@ export class LANPage { this.lanDisabled = 'NotTor' } - this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid - if (this.config.isTor()) { this.fullDocumentationLink = `http://${this.torDocs}` } else { diff --git a/ui/src/app/services/api/api-types.ts b/ui/src/app/services/api/api-types.ts index 6f04ef4a9..2259f37f1 100644 --- a/ui/src/app/services/api/api-types.ts +++ b/ui/src/app/services/api/api-types.ts @@ -1,5 +1,5 @@ import { ConfigSpec } from 'src/app/app-config/config-types' -import { AppAvailableFull, AppInstalledFull } from 'src/app/models/app-types' +import { AppAvailableFull, AppInstalledFull, AppInstalledPreview } from 'src/app/models/app-types' import { Rules } from '../../models/app-model' import { SSHFingerprint, ServerStatus, ServerSpecs } from '../../models/server-model' @@ -23,7 +23,9 @@ export interface ApiServer { /** APPS **/ export type ApiAppAvailableFull = Omit -export type ApiAppInstalledFull = Omit + +export type ApiAppInstalledPreview = Omit +export type ApiAppInstalledFull = Omit export interface ApiAppConfig { spec: ConfigSpec diff --git a/ui/src/app/services/api/api.service.factory.ts b/ui/src/app/services/api/api.service.factory.ts index d0db9a798..12a07ef7b 100644 --- a/ui/src/app/services/api/api.service.factory.ts +++ b/ui/src/app/services/api/api.service.factory.ts @@ -7,8 +7,8 @@ import { ConfigService } from '../config.service' export function ApiServiceFactory (config: ConfigService, http: HttpService, appModel: AppModel, serverModel: ServerModel) { if (config.api.useMocks) { - return new MockApiService(appModel, serverModel) + return new MockApiService(appModel, serverModel, config) } else { - return new LiveApiService(http, appModel, serverModel) + return new LiveApiService(http, appModel, serverModel, config) } } diff --git a/ui/src/app/services/api/api.service.ts b/ui/src/app/services/api/api.service.ts index f98d008a1..bf31d38ba 100644 --- a/ui/src/app/services/api/api.service.ts +++ b/ui/src/app/services/api/api.service.ts @@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model' import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model' import { Subject, Observable } from 'rxjs' -import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull } from './api-types' +import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types' import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util' import { ConfigSpec } from 'src/app/app-config/config-types' @@ -102,7 +102,7 @@ export module ReqRes { export type GetAppLogsRes = string[] export type GetServerLogsRes = string[] export type GetAppMetricsRes = AppMetricsVersioned - export type GetAppsInstalledRes = AppInstalledPreview[] + export type GetAppsInstalledRes = ApiAppInstalledPreview[] export type PostInstallAppReq = { version: string } export type PostInstallAppRes = ApiAppInstalledFull & { breakages: DependentBreakage[] } export type PostUpdateAgentReq = { version: string } diff --git a/ui/src/app/services/api/live-api.service.ts b/ui/src/app/services/api/live-api.service.ts index f71963883..00dea8436 100644 --- a/ui/src/app/services/api/live-api.service.ts +++ b/ui/src/app/services/api/live-api.service.ts @@ -4,8 +4,8 @@ import { AppModel, AppStatus } from '../../models/app-model' import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model' import { ApiService, ReqRes } from './api.service' -import { ApiServer, Unit } from './api-types' -import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types' +import { HttpErrorResponse } from '@angular/common/http' import { isUnauthorized } from 'src/app/util/web.util' import { Replace } from 'src/app/util/types.util' import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util' @@ -13,7 +13,7 @@ import { modulateTime } from 'src/app/util/misc.util' import { Observable, of, throwError } from 'rxjs' import { catchError, mapTo } from 'rxjs/operators' import * as uuid from 'uuid' -import { METHODS } from 'http' +import { ConfigService } from '../config.service' @Injectable() export class LiveApiService extends ApiService { @@ -22,6 +22,7 @@ export class LiveApiService extends ApiService { // TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel private readonly appModel: AppModel, private readonly serverModel: ServerModel, + private readonly config: ConfigService, ) { super() } testConnection(url: string): Promise { @@ -116,11 +117,27 @@ export class LiveApiService extends ApiService { async getInstalledApp(appId: string): Promise { return this.authRequest({ method: Method.GET, url: `/apps/${appId}/installed` }) - .then(app => ({ ...app, hasFetchedFull: true })) + .then(app => { + return { + ...app, + hasFetchedFull: true, + hasUI: this.config.hasUI(app), + launchable: this.config.isLaunchable(app), + } + }) } async getInstalledApps(): Promise { return this.authRequest({ method: Method.GET, url: `/apps/installed` }) + .then(apps => { + return apps.map(app => { + return { + ...app, + hasUI: this.config.hasUI(app), + launchable: this.config.isLaunchable(app), + } + }) + }) } async getAppConfig(appId: string): Promise { @@ -145,7 +162,14 @@ export class LiveApiService extends ApiService { version, } return this.authRequest({ method: Method.POST, url: `/apps/${appId}/install${dryRunParam(dryRun, true)}`, data }) - .then(res => ({ ...res, hasFetchedFull: false })) + .then(app => { + return { + ...app, + hasFetchedFull: false, + hasUI: this.config.hasUI(app), + launchable: this.config.isLaunchable(app), + } + }) } async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { diff --git a/ui/src/app/services/api/mock-api.service.ts b/ui/src/app/services/api/mock-api.service.ts index eedbea523..915381942 100644 --- a/ui/src/app/services/api/mock-api.service.ts +++ b/ui/src/app/services/api/mock-api.service.ts @@ -4,9 +4,10 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model' import { pauseFor } from '../../util/misc.util' import { ApiService, ReqRes } from './api.service' -import { ApiServer, Unit as EmptyResponse, Unit } from './api-types' +import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types' import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util' import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures' +import { ConfigService } from '../config.service' //@TODO consider moving to test folders. @Injectable() @@ -16,6 +17,7 @@ export class MockApiService extends ApiService { constructor ( private readonly appModel: AppModel, private readonly serverModel: ServerModel, + private readonly config: ConfigService, ) { super() } @@ -107,6 +109,14 @@ export class MockApiService extends ApiService { async getInstalledApp (appId: string): Promise { return mockGetInstalledApp(appId) + .then(app => { + return { + ...app, + hasFetchedFull: false, + hasUI: this.hasUI(app), + launchable: this.isLaunchable(app), + } + }) } async getAppMetrics (appId: string): Promise { @@ -115,6 +125,15 @@ export class MockApiService extends ApiService { async getInstalledApps (): Promise { return mockGetInstalledApps() + .then(apps => { + return apps.map(app => { + return { + ...app, + hasUI: this.hasUI(app), + launchable: this.isLaunchable(app), + } + }) + }) } async getAppConfig (appId: string): Promise { @@ -131,6 +150,14 @@ export class MockApiService extends ApiService { async installApp (appId: string, version: string, dryRun: boolean): Promise { return mockInstallApp(appId) + .then(app => { + return { + ...app, + hasFetchedFull: true, + hasUI: this.hasUI(app), + launchable: this.isLaunchable(app), + } + }) } async uninstallApp (appId: string, dryRun: boolean): Promise<{ breakages: DependentBreakage[] }> { @@ -230,7 +257,6 @@ export class MockApiService extends ApiService { } async serviceAction (appId: string, action: ServiceAction): Promise { - console.log('service action', appId, action) await pauseFor(1000) return { jsonrpc: '2.0', @@ -243,9 +269,22 @@ export class MockApiService extends ApiService { } } - refreshLAN (): Promise { + async refreshLAN (): Promise { return mockRefreshLAN() } + + private hasUI (app: ApiAppInstalledPreview): boolean { + return app.lanUi || app.torUi + } + + private isLaunchable (app: ApiAppInstalledPreview): boolean { + return !this.config.isConsulate && + app.status === AppStatus.RUNNING && + ( + (app.torAddress && app.torUi && this.config.isTor()) || + (app.lanAddress && app.lanUi && !this.config.isTor()) + ) + } } async function mockGetServer (): Promise { @@ -294,12 +333,12 @@ async function mockGetAvailableApps (): Promise { return Object.values(mockApiAppAvailableFull) } -async function mockGetInstalledApp (appId: string): Promise { +async function mockGetInstalledApp (appId: string): Promise { await pauseFor(1000) - return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true } + return { ...mockApiAppInstalledFull[appId] } } -async function mockGetInstalledApps (): Promise { +async function mockGetInstalledApps (): Promise { await pauseFor(1000) return Object.values(mockApiAppInstalledFull).map(toInstalledPreview).filter(({ versionInstalled}) => !!versionInstalled) } @@ -329,9 +368,9 @@ async function mockGetAppConfig (): Promise { return mockApiAppConfig } -async function mockInstallApp (appId: string): Promise { +async function mockInstallApp (appId: string): Promise { await pauseFor(1000) - return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true, ...mockAppDependentBreakages } + return { ...mockApiAppInstalledFull[appId], ...mockAppDependentBreakages } } async function mockUninstallApp (): Promise< { breakages: DependentBreakage[] } > { diff --git a/ui/src/app/services/api/mock-app-fixures.ts b/ui/src/app/services/api/mock-app-fixures.ts index 9031527e4..6fbd2d0e7 100644 --- a/ui/src/app/services/api/mock-app-fixures.ts +++ b/ui/src/app/services/api/mock-app-fixures.ts @@ -1,6 +1,7 @@ import { AppStatus } from '../../models/app-model' import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppDependency, BaseApp, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types' import { modulateTime } from 'src/app/util/misc.util' +import { ApiAppInstalledFull } from './api-types' export function toAvailablePreview (f: AppAvailableFull): AppAvailablePreview { return { @@ -23,8 +24,11 @@ export function toInstalledPreview (f: AppInstalledFull): AppInstalledPreview { title: f.title, iconURL: f.iconURL, torAddress: f.torAddress, - ui: f.ui, lanAddress: f.lanAddress, + lanUi: f.lanUi, + torUi: f.torUi, + hasUI: f.hasUI, + launchable: f.launchable, } } @@ -45,7 +49,7 @@ export function toServiceBreakage (f: BaseApp): DependentBreakage { } } -export const bitcoinI: AppInstalledFull = { +export const bitcoinI: ApiAppInstalledFull = { id: 'bitcoind', versionInstalled: '0.18.1', lanAddress: 'bitcoinLan.local', @@ -57,8 +61,8 @@ export const bitcoinI: AppInstalledFull = { instructions: 'some instructions', lastBackup: new Date().toISOString(), configuredRequirements: [], - hasFetchedFull: true, - ui: false, + lanUi: false, + torUi: false, restoreAlert: 'if you restore this app horrible things will happen to the people you love.', actions: [ { id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]}, @@ -66,14 +70,14 @@ export const bitcoinI: AppInstalledFull = { ], } -export const lightningI: AppInstalledFull = { - id: 'c-lightning', +export const lightningI: ApiAppInstalledFull = { + id: 'lightning', lanAddress: 'lightningLan.local', status: AppStatus.RUNNING, title: 'C Lightning', versionInstalled: '1.0.0', torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion', - iconURL: 'assets/img/service-icons/bitwarden.png', + iconURL: 'assets/img/service-icons/c-lightning.png', instructions: 'some instructions', lastBackup: new Date().toISOString(), configuredRequirements: [ @@ -86,12 +90,12 @@ export const lightningI: AppInstalledFull = { violation: null, }), ], - hasFetchedFull: true, - ui: true, + lanUi: false, + torUi: true, actions: [], } -export const cupsI: AppInstalledFull = { +export const cupsI: ApiAppInstalledFull = { id: 'cups', lanAddress: 'cupsLan.local', versionInstalled: '2.1.0', @@ -102,7 +106,6 @@ export const cupsI: AppInstalledFull = { instructions: 'some instructions', lastBackup: new Date().toISOString(), - ui: true, uninstallAlert: 'This is A GREAT APP man, I just don\'t know', configuredRequirements: [ toServiceRequirement(lightningI, @@ -133,7 +136,8 @@ export const cupsI: AppInstalledFull = { violation: { name: 'incompatible-config', ruleViolations: ['bro', 'seriously', 'fix this'] }, }), ], - hasFetchedFull: true, + lanUi: true, + torUi: true, actions: [], } @@ -296,7 +300,7 @@ export const mockApiAppAvailableFull: { [appId: string]: AppAvailableFull; } = { bitwarden: bitwardenA, } -export const mockApiAppInstalledFull: { [appId: string]: AppInstalledFull; } = { +export const mockApiAppInstalledFull: { [appId: string]: ApiAppInstalledFull; } = { bitcoind: bitcoinI, cups: cupsI, lightning: lightningI, diff --git a/ui/src/app/services/config.service.ts b/ui/src/app/services/config.service.ts index 1fa99f7a0..91e9369b3 100644 --- a/ui/src/app/services/config.service.ts +++ b/ui/src/app/services/config.service.ts @@ -1,4 +1,6 @@ import { Injectable } from '@angular/core' +import { AppStatus } from '../models/app-model' +import { ApiAppInstalledPreview } from './api/api-types' const { useMocks, mockOver, skipStartupAlerts } = require('../../../use-mocks.json') as UseMocks @@ -22,12 +24,25 @@ export class ConfigService { } skipStartupAlerts = skipStartupAlerts - isConsulateIos = window['platform'] === 'ios' - isConsulateAndroid = window['platform'] === 'android' + isConsulate = window['platform'] === 'ios' isTor () : boolean { return (this.api.useMocks && mockOver === 'tor') || this.origin.endsWith('.onion') } + + hasUI (app: ApiAppInstalledPreview): boolean { + return app.lanUi || app.torUi + } + + isLaunchable (app: ApiAppInstalledPreview): boolean { + return !this.isConsulate && + app.status === AppStatus.RUNNING && + ( + (app.torAddress && app.torUi && this.isTor()) || + (app.lanAddress && app.lanUi && !this.isTor()) + ) + } + } function removeProtocol (str: string): string {