mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
Integration/0.2.13 (#324)
* updates to 8.10.4, adjusts dependencies, adds license info feature * add toJSON * add licesne info to services * remove mocks * adds license info to available show * prepare upgrade messaging * better welcome message * update backend versioning to 0.2.13 * add version migration file * update ui build scripts * update eos image * update eos image with embassy * add migration files * update welcome page * explicity add migration files Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com> Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -1,27 +1,17 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title >
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.12!</ion-label>
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.13!</ion-label>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<h2>Highlights</h2>
|
||||
<h2>Highlights: 0.2.13</h2>
|
||||
<div class="main-content">
|
||||
<p>This release includes several bugfixes to resolve:</p>
|
||||
<ol>
|
||||
<li>Faster file upload/download ability</li>
|
||||
<li>More robust support for .local addresses</li>
|
||||
<li>Refreshing error messages during configuration changes</li>
|
||||
<li>Starting services with uninstalled optional dependencies</li>
|
||||
<li>Uninstalling services with optional dependencies</li>
|
||||
<li>Redirecting to HTTPS when navigating to LAN address</li>
|
||||
<li>Displaying warning messages during concurrent upgrades of dependent services</li>
|
||||
<li>Allowing larger file uploads</li>
|
||||
<li>Patching a security fix for Tor</li>
|
||||
</ol>
|
||||
<p>At long last, Matrix has arrived!</p>
|
||||
<p>This release also enables displaying Service license information and contains utilities to facilitate the next major release of EmbassyOS.</p>
|
||||
</div>
|
||||
|
||||
<div class="close-button">
|
||||
|
||||
@@ -18,9 +18,11 @@ export interface AppAvailablePreview extends BaseApp {
|
||||
}
|
||||
|
||||
export type AppAvailableFull =
|
||||
AppAvailablePreview &
|
||||
{ descriptionLong: string
|
||||
AppAvailablePreview & {
|
||||
descriptionLong: string
|
||||
versions: string[]
|
||||
licenseName?: string // @TODO required for 0.3.0
|
||||
licenseLink?: string // @TODO required for 0.3.0
|
||||
} &
|
||||
AppAvailableVersionSpecificInfo
|
||||
|
||||
@@ -45,6 +47,8 @@ export interface AppInstalledPreview extends BaseApp {
|
||||
}
|
||||
|
||||
export interface AppInstalledFull extends AppInstalledPreview {
|
||||
licenseName?: string // @TODO required for 0.3.0
|
||||
licenseLink?: string // @TODO required for 0.3.0
|
||||
instructions: string | null
|
||||
lastBackup: string | null
|
||||
configuredRequirements: AppDependency[] | null // null if not yet configured
|
||||
|
||||
@@ -21,6 +21,26 @@
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
|
||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<b>Get ready. View the update instructions.</b>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Now Available...</ion-card-subtitle>
|
||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<b>View the update instructions.</b>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
|
||||
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
|
||||
<ion-avatar style="margin-top: 8px;" slot="start">
|
||||
|
||||
@@ -3,4 +3,14 @@
|
||||
font-style: italic;
|
||||
font-family: 'Open Sans';
|
||||
padding: 1px 0px 1.5px 0px;
|
||||
}
|
||||
|
||||
.instructions-card {
|
||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
|
||||
margin: 16px 10px;
|
||||
}
|
||||
|
||||
.available-card {
|
||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
|
||||
margin: 16px 10px;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { V1Status } from 'src/app/services/api/api-types'
|
||||
|
||||
@Component({
|
||||
selector: 'app-available-list',
|
||||
@@ -20,6 +21,7 @@ export class AppAvailableListPage {
|
||||
installedAppDeltaSubscription: Subscription
|
||||
apps: PropertySubjectId<AppAvailablePreview>[] = []
|
||||
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
|
||||
v1Status: V1Status = { status: 'nothing', version: '' }
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
@@ -35,6 +37,7 @@ export class AppAvailableListPage {
|
||||
|
||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||
this.getApps(),
|
||||
this.checkV1Status(),
|
||||
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
|
||||
pauseFor(600),
|
||||
]))
|
||||
@@ -44,6 +47,14 @@ export class AppAvailableListPage {
|
||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
||||
}
|
||||
|
||||
async checkV1Status () {
|
||||
try {
|
||||
this.v1Status = await this.apiService.checkV1Status()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
mergeInstalledProps (appInstalledId: string) {
|
||||
const appAvailable = this.apps.find(app => app.id === appInstalledId)
|
||||
if (!appAvailable) return
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
versionInstalled: $app$.versionInstalled | async,
|
||||
versionViewing: $app$.versionViewing | async,
|
||||
descriptionLong: $app$.descriptionLong | async,
|
||||
licenseName: $app$.licenseName | async,
|
||||
licenseLink: $app$.licenseLink | async,
|
||||
serviceRequirements: $app$.serviceRequirements | async,
|
||||
iconURL: $app$.iconURL | async,
|
||||
releaseNotes: $app$.releaseNotes | async
|
||||
@@ -112,9 +114,14 @@
|
||||
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
|
||||
</ng-container>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
|
||||
<ion-label>License</ion-label>
|
||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item lines="none" button (click)="presentAlertVersions()">
|
||||
<ion-icon color="medium" slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-label color="medium">Other versions</ion-label>
|
||||
<ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-label>Other versions</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
torAddress: app.torAddress | async,
|
||||
status: app.status | async,
|
||||
versionInstalled: app.versionInstalled | async,
|
||||
licenseName: app.licenseName | async,
|
||||
licenseLink: app.licenseLink | async,
|
||||
configuredRequirements: app.configuredRequirements | async,
|
||||
lastBackup: app.lastBackup | async,
|
||||
hasFetchedFull: app.hasFetchedFull | async,
|
||||
@@ -157,6 +159,12 @@
|
||||
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- license -->
|
||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">License</ion-text></ion-label>
|
||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<!-- dependencies -->
|
||||
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">
|
||||
|
||||
@@ -37,3 +37,7 @@ export interface ApiAppConfig {
|
||||
|
||||
export type Unit = { never?: never; } // hack for the unit typ
|
||||
|
||||
export type V1Status = {
|
||||
status: 'nothing' | 'instructions' | 'available'
|
||||
version: string
|
||||
}
|
||||
|
||||
@@ -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, ApiAppInstalledPreview } from './api-types'
|
||||
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
|
||||
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
|
||||
import { ConfigSpec } from 'src/app/app-config/config-types'
|
||||
|
||||
@@ -64,6 +64,7 @@ export abstract class ApiService {
|
||||
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
||||
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
|
||||
abstract refreshLAN (): Promise<Unit>
|
||||
abstract checkV1Status (): Promise<V1Status>
|
||||
}
|
||||
|
||||
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { ApiAppInstalledPreview, ApiServer, Unit } from './api-types'
|
||||
import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } 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'
|
||||
@@ -17,7 +17,7 @@ import { ConfigService } from '../config.service'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor(
|
||||
constructor (
|
||||
private readonly http: HttpService,
|
||||
// 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,
|
||||
@@ -25,40 +25,40 @@ export class LiveApiService extends ApiService {
|
||||
private readonly config: ConfigService,
|
||||
) { super() }
|
||||
|
||||
testConnection(url: string): Promise<true> {
|
||||
testConnection (url: string): Promise<true> {
|
||||
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
|
||||
}
|
||||
|
||||
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
||||
async getCheckAuth(): Promise<Unit> {
|
||||
async getCheckAuth (): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
||||
}
|
||||
|
||||
async postLogin(password: string): Promise<Unit> {
|
||||
async postLogin (password: string): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
|
||||
}
|
||||
|
||||
async postLogout(): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return {} })
|
||||
async postLogout (): Promise<Unit> {
|
||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
|
||||
}
|
||||
|
||||
async getServer(timeout?: number): Promise<ApiServer> {
|
||||
async getServer (timeout?: number): Promise<ApiServer> {
|
||||
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
|
||||
}
|
||||
|
||||
async acknowledgeOSWelcome(version: string): Promise<Unit> {
|
||||
async acknowledgeOSWelcome (version: string): Promise<Unit> {
|
||||
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
|
||||
}
|
||||
|
||||
async getVersionLatest(): Promise<ReqRes.GetVersionLatestRes> {
|
||||
async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
|
||||
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
|
||||
}
|
||||
|
||||
async getServerMetrics(): Promise<ReqRes.GetServerMetricsRes> {
|
||||
async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
|
||||
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
|
||||
}
|
||||
|
||||
async getNotifications(page: number, perPage: number): Promise<S9Notification[]> {
|
||||
async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
|
||||
const params: ReqRes.GetNotificationsReq = {
|
||||
page: String(page),
|
||||
perPage: String(perPage),
|
||||
@@ -66,27 +66,27 @@ export class LiveApiService extends ApiService {
|
||||
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
|
||||
}
|
||||
|
||||
async deleteNotification(id: string): Promise<Unit> {
|
||||
async deleteNotification (id: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
|
||||
}
|
||||
|
||||
async getExternalDisks(): Promise<DiskInfo[]> {
|
||||
async getExternalDisks (): Promise<DiskInfo[]> {
|
||||
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
|
||||
}
|
||||
|
||||
// TODO: EJECT-DISKS
|
||||
async ejectExternalDisk(logicalName: string): Promise<Unit> {
|
||||
async ejectExternalDisk (logicalName: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
|
||||
}
|
||||
|
||||
async updateAgent(version: string): Promise<Unit> {
|
||||
async updateAgent (version: string): Promise<Unit> {
|
||||
const data: ReqRes.PostUpdateAgentReq = {
|
||||
version: `=${version}`,
|
||||
}
|
||||
return this.authRequest({ method: Method.POST, url: '/update', data })
|
||||
}
|
||||
|
||||
async getAvailableAppVersionSpecificInfo(appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
|
||||
async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
|
||||
return this
|
||||
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
|
||||
.then(res => ({ ...res, versionViewing: res.version }))
|
||||
@@ -96,7 +96,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getAvailableApps(): Promise<AppAvailablePreview[]> {
|
||||
async getAvailableApps (): Promise<AppAvailablePreview[]> {
|
||||
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
|
||||
return res.map(a => {
|
||||
const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
|
||||
@@ -105,7 +105,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getAvailableApp(appId: string): Promise<AppAvailableFull> {
|
||||
async getAvailableApp (appId: string): Promise<AppAvailableFull> {
|
||||
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
|
||||
.then(res => {
|
||||
return {
|
||||
@@ -115,7 +115,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getInstalledApp(appId: string): Promise<AppInstalledFull> {
|
||||
async getInstalledApp (appId: string): Promise<AppInstalledFull> {
|
||||
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
|
||||
.then(app => {
|
||||
return {
|
||||
@@ -127,7 +127,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getInstalledApps(): Promise<AppInstalledPreview[]> {
|
||||
async getInstalledApps (): Promise<AppInstalledPreview[]> {
|
||||
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
|
||||
.then(apps => {
|
||||
return apps.map(app => {
|
||||
@@ -140,24 +140,24 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> {
|
||||
async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
|
||||
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
|
||||
}
|
||||
|
||||
async getAppLogs(appId: string, params: ReqRes.GetAppLogsReq = {}): Promise<string[]> {
|
||||
async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
|
||||
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
|
||||
}
|
||||
|
||||
async getServerLogs(): Promise<string[]> {
|
||||
async getServerLogs (): Promise<string[]> {
|
||||
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
|
||||
}
|
||||
|
||||
async getAppMetrics(appId: string): Promise<AppMetrics> {
|
||||
async getAppMetrics (appId: string): Promise<AppMetrics> {
|
||||
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
|
||||
.then(parseMetricsPermissive)
|
||||
}
|
||||
|
||||
async installApp(appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
|
||||
async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
|
||||
const data: ReqRes.PostInstallAppReq = {
|
||||
version,
|
||||
}
|
||||
@@ -172,94 +172,94 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
}
|
||||
|
||||
async startApp(appId: string): Promise<Unit> {
|
||||
async startApp (appId: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
|
||||
.then(() => ({}))
|
||||
.then(() => ({ }))
|
||||
}
|
||||
|
||||
async stopApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
|
||||
return res
|
||||
}
|
||||
|
||||
async restartApp(appId: string): Promise<Unit> {
|
||||
async restartApp (appId: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
|
||||
.then(() => ({} as any))
|
||||
.then(() => ({ } as any))
|
||||
}
|
||||
|
||||
async createAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAppBackupCreateReq = {
|
||||
password: password || undefined,
|
||||
logicalname,
|
||||
}
|
||||
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
|
||||
.then(() => ({}))
|
||||
.then(() => ({ }))
|
||||
}
|
||||
|
||||
async stopAppBackup(appId: string): Promise<Unit> {
|
||||
async stopAppBackup (appId: string): Promise<Unit> {
|
||||
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
|
||||
.then(() => ({}))
|
||||
.then(() => ({ }))
|
||||
}
|
||||
|
||||
async restoreAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAppBackupRestoreReq = {
|
||||
password: password || undefined,
|
||||
logicalname,
|
||||
}
|
||||
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
|
||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
|
||||
.then(() => ({}))
|
||||
.then(() => ({ }))
|
||||
}
|
||||
|
||||
async patchAppConfig(app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||
const data: ReqRes.PatchAppConfigReq = {
|
||||
config,
|
||||
}
|
||||
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
|
||||
}
|
||||
|
||||
async postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
|
||||
async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||
}
|
||||
|
||||
async patchServerConfig(attr: string, value: any): Promise<Unit> {
|
||||
async patchServerConfig (attr: string, value: any): Promise<Unit> {
|
||||
const data: ReqRes.PatchServerConfigReq = {
|
||||
value,
|
||||
}
|
||||
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
|
||||
.then(() => this.serverModel.update({ [attr]: value }))
|
||||
.then(() => ({}))
|
||||
.then(() => ({ }))
|
||||
}
|
||||
|
||||
async wipeAppData(app: AppInstalledPreview): Promise<Unit> {
|
||||
async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
async toggleAppLAN(appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
|
||||
}
|
||||
|
||||
async addSSHKey(sshKey: string): Promise<Unit> {
|
||||
async addSSHKey (sshKey: string): Promise<Unit> {
|
||||
const data: ReqRes.PostAddSSHKeyReq = {
|
||||
sshKey,
|
||||
}
|
||||
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
|
||||
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
|
||||
return {}
|
||||
return { }
|
||||
}
|
||||
|
||||
async addWifi(ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
|
||||
async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
|
||||
const data: ReqRes.PostAddWifiReq = {
|
||||
ssid,
|
||||
password,
|
||||
@@ -269,30 +269,30 @@ export class LiveApiService extends ApiService {
|
||||
return this.authRequest({ method: Method.POST, url: `/wifi`, data })
|
||||
}
|
||||
|
||||
async connectWifi(ssid: string): Promise<Unit> {
|
||||
async connectWifi (ssid: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
|
||||
}
|
||||
|
||||
async deleteWifi(ssid: string): Promise<Unit> {
|
||||
async deleteWifi (ssid: string): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
|
||||
}
|
||||
|
||||
async deleteSSHKey(fingerprint: SSHFingerprint): Promise<Unit> {
|
||||
async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
|
||||
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
|
||||
const ssh = this.serverModel.peek().ssh
|
||||
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
|
||||
return {}
|
||||
return { }
|
||||
}
|
||||
|
||||
async restartServer(): Promise<Unit> {
|
||||
async restartServer (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
|
||||
}
|
||||
|
||||
async shutdownServer(): Promise<Unit> {
|
||||
async shutdownServer (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
||||
}
|
||||
|
||||
async serviceAction(appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||
async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||
const data: ReqRes.ServiceActionRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: uuid.v4(),
|
||||
@@ -301,11 +301,15 @@ export class LiveApiService extends ApiService {
|
||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
|
||||
}
|
||||
|
||||
async refreshLAN(): Promise<Unit> {
|
||||
async refreshLAN (): Promise<Unit> {
|
||||
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
|
||||
}
|
||||
|
||||
private async authRequest<T>(opts: HttpOptions, overrides: Partial<{ version: string }> = {}): Promise<T> {
|
||||
async checkV1Status (): Promise<V1Status> {
|
||||
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
|
||||
}
|
||||
|
||||
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
||||
|
||||
opts.withCredentials = true
|
||||
@@ -324,7 +328,7 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
|
||||
return first ? `?dryrun` : `&dryrun`
|
||||
}
|
||||
|
||||
function catchHttpStatusError(error: HttpErrorResponse): Observable<true> {
|
||||
function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
return throwError('Not Connected')
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types'
|
||||
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit, V1Status } 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'
|
||||
@@ -273,12 +273,19 @@ export class MockApiService extends ApiService {
|
||||
return mockRefreshLAN()
|
||||
}
|
||||
|
||||
async checkV1Status (): Promise<V1Status> {
|
||||
return {
|
||||
status: 'instructions',
|
||||
version: '1.0.0',
|
||||
}
|
||||
}
|
||||
|
||||
private hasUI (app: ApiAppInstalledPreview): boolean {
|
||||
return app.lanUi || app.torUi
|
||||
}
|
||||
|
||||
private isLaunchable (app: ApiAppInstalledPreview): boolean {
|
||||
return !this.config.isConsulate &&
|
||||
return !this.config.isConsulate &&
|
||||
app.status === AppStatus.RUNNING &&
|
||||
(
|
||||
(app.torAddress && app.torUi && this.config.isTor()) ||
|
||||
@@ -355,7 +362,7 @@ async function mockGetServerLogs (): Promise<ReqRes.GetServerLogsRes> {
|
||||
|
||||
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
|
||||
await pauseFor(1000)
|
||||
return mockApiAppMetricsV1
|
||||
return mockApiAppMetricsV1 as ReqRes.GetAppMetricsRes
|
||||
}
|
||||
|
||||
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
|
||||
@@ -492,7 +499,7 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
|
||||
const mockApiServer: () => ReqRes.GetServerRes = () => ({
|
||||
serverId: 'start9-mockxyzab',
|
||||
name: 'Embassy:12345678',
|
||||
versionInstalled: '0.2.12',
|
||||
versionInstalled: '0.2.13',
|
||||
versionLatest: '0.2.13',
|
||||
status: ServerStatus.RUNNING,
|
||||
alternativeRegistryUrl: 'beta-registry.start9labs.com',
|
||||
|
||||
@@ -54,6 +54,8 @@ export const bitcoinI: ApiAppInstalledFull = {
|
||||
versionInstalled: '0.18.1',
|
||||
lanAddress: undefined,
|
||||
title: 'Bitcoin Core',
|
||||
licenseName: 'MIT',
|
||||
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
|
||||
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
|
||||
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
|
||||
status: AppStatus.STOPPED,
|
||||
@@ -147,6 +149,8 @@ export const bitcoinA: AppAvailableFull = {
|
||||
id: 'bitcoind',
|
||||
versionLatest: '0.19.1.1',
|
||||
versionInstalled: '0.19.0',
|
||||
licenseName: 'MIT',
|
||||
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
|
||||
status: AppStatus.UNKNOWN,
|
||||
title: 'Bitcoin Core',
|
||||
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
|
||||
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
||||
import { S9Server } from '../models/server-model'
|
||||
import { displayEmver } from '../pipes/emver.pipe'
|
||||
import { V1Status } from './api/api-types'
|
||||
import { ApiService, ReqRes } from './api/api.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Emver } from './emver.service'
|
||||
@@ -36,6 +37,13 @@ export class StartupAlertsNotifier {
|
||||
display: vl => this.displayOsUpdateCheck(vl),
|
||||
hasRun: this.config.skipStartupAlerts,
|
||||
}
|
||||
const v1StatusUpdate: Check<V1Status> = {
|
||||
name: 'v1Status',
|
||||
shouldRun: s => this.shouldRunOsUpdateCheck(s),
|
||||
check: () => this.v1StatusCheck(),
|
||||
display: s => this.displayV1Check(s),
|
||||
hasRun: this.config.skipStartupAlerts,
|
||||
}
|
||||
const apps: Check<boolean> = {
|
||||
name: 'apps',
|
||||
shouldRun: s => this.shouldRunAppsCheck(s),
|
||||
@@ -43,7 +51,7 @@ export class StartupAlertsNotifier {
|
||||
display: () => this.displayAppsCheck(),
|
||||
hasRun: this.config.skipStartupAlerts,
|
||||
}
|
||||
this.checks = [welcome, osUpdate, apps]
|
||||
this.checks = [welcome, osUpdate, v1StatusUpdate, apps]
|
||||
}
|
||||
|
||||
// This takes our three checks and filters down to those that should run.
|
||||
@@ -85,6 +93,10 @@ export class StartupAlertsNotifier {
|
||||
return server.autoCheckUpdates
|
||||
}
|
||||
|
||||
private async v1StatusCheck (): Promise<V1Status> {
|
||||
return this.apiService.checkV1Status()
|
||||
}
|
||||
|
||||
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
|
||||
const res = await this.apiService.getVersionLatest()
|
||||
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
|
||||
@@ -131,6 +143,33 @@ export class StartupAlertsNotifier {
|
||||
return true
|
||||
}
|
||||
|
||||
private async displayV1Check (s: V1Status): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
if (s.status !== 'available') return resolve(true)
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: `EmbassyOS ${s.version} Now Available!`,
|
||||
message: `Version ${s.version} introduces SSD support and a whole lot more.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => resolve(true),
|
||||
},
|
||||
{
|
||||
text: 'View Instructions',
|
||||
handler: () => {
|
||||
window.open(`https://start9.com/eos-${s.version}`, '_blank')
|
||||
resolve(false)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
|
||||
private async displayAppsCheck (): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
|
||||
Reference in New Issue
Block a user