update/alpha.9 (#2988)

* import marketplac preview for sideload

* fix: improve state service (#2977)

* fix: fix sideload DI

* fix: update Angular

* fix: cleanup

* fix: fix version selection

* Bump node version to fix build for Angular

* misc fixes
- update node to v22
- fix chroot-and-upgrade access to prune-images
- don't self-migrate legacy packages
- #2985
- move dataVersion to volume folder
- remove "instructions.md" from s9pk
- add "docsUrl" to manifest

* version bump

* include flavor when clicking view listing from updates tab

* closes #2980

* fix: fix select button

* bring back ssh keys

* fix: drop 'portal' from all routes

* fix: implement longtap action to select table rows

* fix description for ssh page

* replace instructions with docsLink and refactor marketplace preview

* delete unused translations

* fix patchdb diffing algorithm

* continue refactor of marketplace lib show components

* Booting StartOS instead of Setting up your server on init

* misc fixes
- closes #2990
- closes #2987

* fix build

* docsUrl and clickable service headers

* don't cleanup after update until new service install succeeds

* update types

* misc fixes

* beta.35

* sdkversion, githash for sideload, correct logs for init, startos pubkey display

* bring back reboot button on install

* misc fixes

* beta.36

* better handling of setup and init for websocket errors

* reopen init and setup logs even on graceful closure

* better logging, misc fixes

* fix build

* dont let package stats hang

* dont show docsurl in marketplace if no docsurl

* re-add needs-config

* show error if init fails, shorten hover state on header icons

* fix operator precedemce

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Mariusz Kogen <k0gen@pm.me>
This commit is contained in:
Aiden McClelland
2025-07-18 18:31:12 +00:00
committed by GitHub
parent ba2906a42e
commit 377b7b12ce
237 changed files with 5953 additions and 4777 deletions

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: {
aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.8/startos-0.4.0-alpha.8-33ae46f~dev_aarch64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_aarch64.squashfs',
commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
},
'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.8/startos-0.4.0-alpha.8-33ae46f~dev_aarch64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
},
raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.8/startos-0.4.0-alpha.8-33ae46f~dev_raspberrypi.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_raspberrypi.squashfs',
commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
},
x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.8/startos-0.4.0-alpha.8-33ae46f~dev_x86_64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_x86_64.squashfs',
commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
},
'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.8/startos-0.4.0-alpha.8-33ae46f~dev_x86_64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.9/startos-0.4.0-alpha.9-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136,
@@ -217,6 +217,7 @@ export namespace Mock {
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
donationUrl: 'https://start9.com',
docsUrl: 'https://docs.start9.com',
alerts: {
install: 'Bitcoin can take over a week to sync.',
uninstall:
@@ -262,6 +263,7 @@ export namespace Mock {
supportSite: 'https://lightning.engineering/',
marketingSite: 'https://lightning.engineering/',
donationUrl: null,
docsUrl: 'https://docs.start9.com',
alerts: {
install: null,
uninstall: null,
@@ -319,6 +321,7 @@ export namespace Mock {
supportSite: '',
marketingSite: '',
donationUrl: 'https://start9.com',
docsUrl: 'https://docs.start9.com',
alerts: {
install: 'Testing install alert',
uninstall: null,
@@ -379,8 +382,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -412,8 +417,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -455,8 +462,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -488,8 +497,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -533,8 +544,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -589,8 +602,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -649,8 +664,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -682,8 +699,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -725,8 +744,10 @@ export namespace Mock {
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -780,9 +801,11 @@ export namespace Mock {
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues',
docsUrl: 'https://github.com/Kixunil/btc-rpc-proxy',
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.36',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,
@@ -1930,7 +1953,7 @@ export namespace Mock {
state: 'installed',
manifest: MockManifestBitcoind,
},
dataVersion: MockManifestBitcoind.version,
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/bitcoind.svg',
lastBackup: null,
status: {
@@ -2204,7 +2227,7 @@ export namespace Mock {
state: 'installed',
manifest: MockManifestBitcoinProxy,
},
dataVersion: MockManifestBitcoinProxy.version,
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
lastBackup: null,
status: {
@@ -2249,7 +2272,7 @@ export namespace Mock {
state: 'installed',
manifest: MockManifestLnd,
},
dataVersion: MockManifestLnd.version,
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
status: {
@@ -2272,7 +2295,7 @@ export namespace Mock {
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
group: 'Connecting',
},
},
serviceInterfaces: {

View File

@@ -9,15 +9,15 @@ export abstract class ApiService {
// for sideloading packages
abstract uploadPackage(guid: string, body: Blob): Promise<void>
// for getting static files: ex icons, instructions, licenses
// for getting static files: ex license
abstract getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string>
abstract getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string>
// websocket

View File

@@ -1,4 +1,6 @@
import { Inject, Injectable } from '@angular/core'
import { DOCUMENT, Inject, Injectable } from '@angular/core'
import { blake3 } from '@noble/hashes/blake3'
import { MarketplacePkg } from '@start9labs/marketplace'
import {
HttpOptions,
HttpService,
@@ -7,18 +9,15 @@ import {
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
import { ApiService } from './embassy-api.service'
import { RR } from './api.types'
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
import { Observable, filter, firstValueFrom } from 'rxjs'
import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
import { DataModel } from '../patch-db/data-model'
import { Dump, pathFromArray } from 'patch-db-client'
import { T } from '@start9labs/start-sdk'
import { MarketplacePkg } from '@start9labs/marketplace'
import { blake3 } from '@noble/hashes/blake3'
import { Dump, pathFromArray } from 'patch-db-client'
import { filter, firstValueFrom, Observable } from 'rxjs'
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
import { AuthService } from '../auth.service'
import { DataModel } from '../patch-db/data-model'
import { RR } from './api.types'
import { ApiService } from './embassy-api.service'
@Injectable()
export class LiveApiService extends ApiService {
@@ -45,11 +44,11 @@ export class LiveApiService extends ApiService {
})
}
// for getting static files: ex. instructions, licenses
// for getting static files: ex: license
async getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string> {
const encodedUrl = encodeURIComponent(pkg.s9pk.url)
@@ -66,7 +65,7 @@ export class LiveApiService extends ApiService {
async getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string> {
return this.httpRequest({
method: Method.GET,

View File

@@ -77,7 +77,7 @@ export class MockApiService extends ApiService {
async getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string> {
await pauseFor(2000)
return markdown
@@ -85,7 +85,7 @@ export class MockApiService extends ApiService {
async getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
path: 'LICENSE.md',
): Promise<string> {
await pauseFor(2000)
return markdown

View File

@@ -12,7 +12,6 @@ export const mockPatchData: DataModel = {
},
startosRegistry: 'https://registry.start9.com/',
snakeHighScore: 0,
ackInstructions: {},
language: 'english',
},
serverInfo: {
@@ -170,7 +169,7 @@ export const mockPatchData: DataModel = {
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
packageVersionCompat: '>=0.3.0 <=0.3.6',
postInitMigrationTodos: [],
postInitMigrationTodos: {},
statusInfo: {
// currentBackup: null,
updated: false,
@@ -200,7 +199,7 @@ export const mockPatchData: DataModel = {
version: '0.20.0:0-alpha.1',
},
},
dataVersion: '0.20.0:0',
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/bitcoind.svg',
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
status: {
@@ -480,7 +479,7 @@ export const mockPatchData: DataModel = {
version: '0.11.0:0.0.1',
},
},
dataVersion: '0.11.0:0.0.1',
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
icon: '/assets/img/service-icons/lnd.png',
lastBackup: null,
status: {
@@ -503,7 +502,7 @@ export const mockPatchData: DataModel = {
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
group: 'Connecting',
},
},
serviceInterfaces: {

View File

@@ -77,13 +77,13 @@ export class BadgeService {
getCount(id: string): Observable<number> {
switch (id) {
case '/portal/updates':
case 'updates':
return this.updates$
case '/portal/system':
case 'system':
return this.system$
case '/portal/metrics':
case 'metrics':
return this.metrics$
case '/portal/notifications':
case 'notifications':
return this.notifications.unreadCount$
default:
return EMPTY

View File

@@ -1,5 +1,4 @@
import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core'
import { Inject, Injectable, DOCUMENT } from '@angular/core'
import { WorkspaceConfig } from '@start9labs/shared'
import { T, utils } from '@start9labs/start-sdk'
import { PackageDataEntry } from './patch-db/data-model'

View File

@@ -115,6 +115,7 @@ export class MarketplaceService {
flavor: string | null,
registryUrl?: string,
): Observable<MarketplacePkg> {
console.log('HERE')
return this.currentRegistry$.pipe(
switchMap(registry => {
const url = registryUrl || registry.url
@@ -141,11 +142,8 @@ export class MarketplaceService {
)
}
fetchStatic$(
pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md',
): Observable<string> {
return from(this.api.getStaticProxy(pkg, type))
fetchStatic$(pkg: MarketplacePkg): Observable<string> {
return from(this.api.getStaticProxy(pkg, 'LICENSE.md'))
}
private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {

View File

@@ -6,7 +6,6 @@ export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData }
export type UIData = {
name: string | null
registries: Record<string, string | null>
ackInstructions: Record<string, boolean>
snakeHighScore: number
startosRegistry: string
language: Languages

View File

@@ -33,7 +33,7 @@ export class StandardActionsService {
try {
await this.api.rebuildPackage({ id })
await this.router.navigate(['portal', 'services', id])
await this.router.navigate(['services', id])
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -80,8 +80,7 @@ export class StandardActionsService {
try {
await this.api.uninstallPackage(options)
await this.api.setDbValue<boolean>(['ackInstructions', options.id], false)
await this.router.navigate(['portal'])
await this.router.navigate([''])
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -1,19 +1,18 @@
import { inject, Injectable } from '@angular/core'
import { Component, inject, Injectable } from '@angular/core'
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk'
import { TuiAlertService } from '@taiga-ui/core'
import { TuiAlertService, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import {
BehaviorSubject,
combineLatest,
concat,
EMPTY,
exhaustMap,
from,
merge,
Observable,
startWith,
Subject,
timer,
} from 'rxjs'
import {
@@ -38,6 +37,22 @@ const OPTIONS: IsActiveMatchOptions = {
matrixParams: 'ignored',
}
@Component({
template: `
<tui-loader size="m" [inheritColor]="true" />
<div tuiTitle>
{{ 'State unknown' | i18n }}
<span tuiSubtitle>
{{ 'Trying to reach server' | i18n }}
</span>
</div>
`,
host: { style: 'padding: 0 0.25rem' },
imports: [i18nPipe, TuiLoader, TuiTitle],
hostDirectives: [TuiCell],
})
class DisconnectedToast {}
@Injectable({
providedIn: 'root',
})
@@ -47,83 +62,75 @@ export class StateService extends Observable<RR.ServerState | null> {
private readonly api = inject(ApiService)
private readonly router = inject(Router)
private readonly network$ = inject(NetworkService)
private readonly single$ = new Subject<RR.ServerState>()
private readonly trigger$ = new BehaviorSubject<void>(undefined)
private readonly poll$ = this.trigger$.pipe(
private readonly trigger$ = new BehaviorSubject(true)
private readonly disconnected$ = this.alerts.open(
new PolymorpheusComponent(DisconnectedToast),
{ closeable: false, appearance: 'negative', icon: '' },
)
private readonly reconnected$ = this.alerts.open(
this.i18n.transform('Connection restored'),
{ label: this.i18n.transform('Server connected'), appearance: 'positive' },
)
private readonly stream$ = this.trigger$.pipe(
switchMap(() => this.network$.pipe(filter(Boolean))),
switchMap(() =>
timer(0, 2000).pipe(
switchMap(() =>
exhaustMap(() =>
from(this.api.getState()).pipe(catchError(() => EMPTY)),
),
take(1),
),
),
)
private readonly stream$ = merge(this.single$, this.poll$).pipe(
tap(state => {
switch (state) {
case 'initializing':
this.router.navigate(['initializing'], { replaceUrl: true })
break
case 'error':
this.router.navigate(['diagnostic'], { replaceUrl: true })
break
case 'running':
if (
this.router.isActive('initializing', OPTIONS) ||
this.router.isActive('diagnostic', OPTIONS)
) {
this.router.navigate([''], { replaceUrl: true })
}
break
}
}),
tap(state => this.handleState(state)),
startWith(null),
shareReplay(1),
)
private readonly alert = merge(
this.trigger$.pipe(skip(1)),
this.network$.pipe(filter(v => !v)),
)
.pipe(
exhaustMap(() =>
concat(
this.alerts
.open(this.i18n.transform('Trying to reach server'), {
label: this.i18n.transform('State unknown'),
closeable: false,
appearance: 'negative',
})
.pipe(
takeUntil(
combineLatest([this.stream$.pipe(skip(1)), this.network$]).pipe(
filter(state => state.every(Boolean)),
),
),
),
this.alerts.open(this.i18n.transform('Connection restored'), {
label: this.i18n.transform('Server connected'),
appearance: 'positive',
}),
),
),
)
.subscribe()
constructor() {
super(subscriber => this.stream$.subscribe(subscriber))
// Retrigger on offline
this.network$.pipe(filter(v => !v)).subscribe(() => this.retrigger())
// Show toasts
this.trigger$
.pipe(
filter(v => !v),
exhaustMap(() =>
concat(
this.disconnected$.pipe(takeUntil(this.stream$.pipe(skip(1)))),
this.reconnected$,
),
),
)
.subscribe()
}
retrigger() {
this.trigger$.next()
retrigger(gracefully = false) {
this.trigger$.next(gracefully)
}
async syncState() {
const state = await this.api.getState()
this.single$.next(state)
private handleState(state: RR.ServerState): void {
switch (state) {
case 'initializing':
this.router.navigate(['initializing'], { replaceUrl: true })
break
case 'error':
this.router.navigate(['diagnostic'], { replaceUrl: true })
break
case 'running':
if (
this.router.isActive('initializing', OPTIONS) ||
this.router.isActive('diagnostic', OPTIONS)
) {
this.router.navigate([''], { replaceUrl: true })
}
break
}
}
}

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@angular/core'
import { DOCUMENT } from '@angular/common'
import { Inject, Injectable, DOCUMENT } from '@angular/core'
const PREFIX = '_startos/'