Compare commits

..

15 Commits

Author SHA1 Message Date
Matt Hill
1f6f3dc72d bump sdk 2026-03-06 00:22:01 -07:00
Matt Hill
59155c2e34 remove non option from smtp for better package compat 2026-03-06 00:06:18 -07:00
Matt Hill
7693b0febc multiple bugs, better outbound gateway UX 2026-03-05 23:20:13 -07:00
Matt Hill
3901d38d65 various bug, improve smtp 2026-03-05 18:28:34 -07:00
Aiden McClelland
8fdeeab5bb sdk beta.56 2026-03-05 15:12:50 -07:00
Aiden McClelland
fd1ccc0c8c fix race condition in wan ip check 2026-03-05 15:04:28 -07:00
Aiden McClelland
d31f762d5a preserve usb as top efi boot option 2026-03-05 15:04:06 -07:00
Aiden McClelland
5a0cd302de fix starttls 2026-03-05 09:44:05 -07:00
Matt Hill
e71023a3a7 better shared hostname approach, and improve look-feel of addresses tables 2026-03-04 23:24:08 -07:00
Aiden McClelland
e077b5425b fix: scope public domain to single binding and return single port check
Accept internalPort in AddPublicDomainParams to target a specific
binding. Disable the domain on all other bindings. Return a single
CheckPortRes instead of Vec. Revert multi-port UI to singular port
display from 0f8a66b35.
2026-03-04 21:43:34 -07:00
Aiden McClelland
d982ffa722 feat: add shared host note to private domain dialog with i18n 2026-03-04 17:44:30 -07:00
Aiden McClelland
4005365239 feat: inline domain health checks and improve address UX
- addPublicDomain returns DNS query + port check results (AddPublicDomainRes)
  so frontend skips separate API calls after adding a domain
- addPrivateDomain returns check_dns result for the gateway
- Support multiple ports per domain in validation modal (deduplicated)
- Run port checks concurrently via futures::future::join_all
- Add note to add-domain dialog showing other interfaces on same host
- Add addXForwardedHeaders to knownProtocols in SDK Host.ts
- Add plugin filter kind, pluginId filter, matchesAny, and docs to
  getServiceInterface.ts
- Add PassthroughInfo type and passthroughs field to NetworkInfo
- Pluralize "port forwarding rules" in i18n dictionaries
2026-03-04 17:30:00 -07:00
Aiden McClelland
0f8a66b357 passthrough feature 2026-03-04 16:32:21 -07:00
Aiden McClelland
2ed8402edd fixes for build scripts 2026-03-04 16:31:57 -07:00
Matt Hill
f7f87a4e6a task fix and keyboard fix 2026-03-04 14:02:40 -07:00
67 changed files with 1811 additions and 1876 deletions

View File

@@ -524,26 +524,26 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
"To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n",
" - MacOS\n",
" 1. Open the Terminal app\n",
" 2. Type or copy/paste the following command (**DO NOT** click Enter/Return yet): pbpaste > ~/Desktop/tunnel-ca.crt\n",
" 2. Paste the following command (**DO NOT** click Return): pbcopy < ~/Desktop/ca.crt\n",
" 3. Copy your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 4. Back in Terminal, click Enter/Return. tunnel-ca.crt is saved to your Desktop\n",
" 5. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Mac\n",
" 4. Back in Terminal, click Return. ca.crt is saved to your Desktop\n",
" 5. Complete by trusting your Root CA: https://docs.start9.com/device-guides/mac/ca.html\n",
" - Linux\n",
" 1. Open gedit, nano, or any editor\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Debian+%252F+Ubuntu\n",
" 3. Name the file ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/linux/ca.html\n",
" - Windows\n",
" 1. Open the Notepad app\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Windows\n",
" 3. Name the file ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/device-guides/windows/ca.html\n",
" - Android/Graphene\n",
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Android+%252F+Graphene\n",
" 1. Send the ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/android/ca.html\n",
" - iOS\n",
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=iOS\n",
" 1. Send the ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/device-guides/ios/ca.html\n",
));
return Ok(());

2502
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,33 +33,34 @@
"format:check": "prettier --check projects/"
},
"dependencies": {
"@angular/animations": "^21.2.1",
"@angular/cdk": "^21.2.1",
"@angular/common": "^21.2.1",
"@angular/compiler": "^21.2.1",
"@angular/core": "^21.2.1",
"@angular/forms": "^21.2.1",
"@angular/platform-browser": "^21.2.1",
"@angular/pwa": "^21.2.1",
"@angular/router": "^21.2.1",
"@angular/service-worker": "^21.2.1",
"@angular/animations": "^20.3.0",
"@angular/cdk": "^20.1.0",
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/platform-browser-dynamic": "^20.1.0",
"@angular/pwa": "^20.3.0",
"@angular/router": "^20.3.0",
"@angular/service-worker": "^20.3.0",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@noble/curves": "^1.4.0",
"@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.3.0",
"@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.73.0",
"@taiga-ui/addon-commerce": "4.73.0",
"@taiga-ui/addon-mobile": "4.73.0",
"@taiga-ui/addon-table": "4.73.0",
"@taiga-ui/cdk": "4.73.0",
"@taiga-ui/core": "4.73.0",
"@taiga-ui/addon-charts": "4.66.0",
"@taiga-ui/addon-commerce": "4.66.0",
"@taiga-ui/addon-mobile": "4.66.0",
"@taiga-ui/addon-table": "4.66.0",
"@taiga-ui/cdk": "4.66.0",
"@taiga-ui/core": "4.66.0",
"@taiga-ui/dompurify": "4.1.11",
"@taiga-ui/event-plugins": "4.7.0",
"@taiga-ui/experimental": "4.73.0",
"@taiga-ui/icons": "4.73.0",
"@taiga-ui/kit": "4.73.0",
"@taiga-ui/layout": "4.73.0",
"@taiga-ui/experimental": "4.66.0",
"@taiga-ui/icons": "4.66.0",
"@taiga-ui/kit": "4.66.0",
"@taiga-ui/layout": "4.66.0",
"@taiga-ui/polymorpheus": "4.9.0",
"ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1",
@@ -79,7 +80,7 @@
"mime": "^4.0.3",
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^21.0.0",
"ng-qrcode": "^20.0.0",
"node-jose": "^2.2.0",
"patch-db-client": "file:../patch-db/client",
"pbkdf2": "^3.1.2",
@@ -91,10 +92,10 @@
},
"devDependencies": {
"@angular-experts/hawkeye": "^1.7.2",
"@angular/build": "^21.2.1",
"@angular/cli": "^21.2.1",
"@angular/compiler-cli": "^21.2.1",
"@angular/language-service": "^21.2.1",
"@angular/build": "^20.1.0",
"@angular/cli": "^20.1.0",
"@angular/compiler-cli": "^20.1.0",
"@angular/language-service": "^20.1.0",
"@types/dompurify": "3.0.5",
"@types/estree": "^0.0.51",
"@types/js-yaml": "^4.0.5",
@@ -106,7 +107,7 @@
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",
"lint-staged": "^13.2.0",
"ng-packagr": "^21.2.0",
"ng-packagr": "^20.1.0",
"node-html-parser": "^5.3.3",
"postcss": "^8.4.21",
"prettier": "^3.5.3",

View File

@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import {
DocsLinkDirective,
i18nPipe,
SharedPipesModule,
} from '@start9labs/shared'
import {
TuiAppearance,
TuiButton,
TuiIcon,
TuiLoader,
TuiPopup,
} from '@taiga-ui/core'
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
import { CategoriesModule } from '../../pages/list/categories/categories.module'
import { SearchModule } from '../../pages/list/search/search.module'
import { StoreIconComponentModule } from '../store-icon/store-icon.component.module'
import { MenuComponent } from './menu.component'
@NgModule({
imports: [
CommonModule,
SharedPipesModule,
SearchModule,
CategoriesModule,
TuiLoader,
TuiButton,
CategoriesModule,
StoreIconComponentModule,
TuiAppearance,
TuiIcon,
TuiSkeleton,
TuiDrawer,
TuiPopup,
i18nPipe,
DocsLinkDirective,
],
declarations: [MenuComponent],
exports: [MenuComponent],
})
export class MenuModule {}

View File

@@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -7,35 +6,16 @@ import {
OnDestroy,
signal,
} from '@angular/core'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import { TuiAppearance, TuiButton, TuiIcon, TuiPopup } from '@taiga-ui/core'
import { TuiDrawer, TuiSkeleton } from '@taiga-ui/kit'
import { Subject, takeUntil } from 'rxjs'
import { CategoriesComponent } from '../../pages/list/categories/categories.component'
import { SearchComponent } from '../../pages/list/search/search.component'
import { AbstractCategoryService } from '../../services/category.service'
import { StoreDataWithUrl } from '../../types'
import { StoreIconComponent } from '../store-icon.component'
@Component({
selector: 'menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss'],
imports: [
CommonModule,
SearchComponent,
CategoriesComponent,
TuiButton,
StoreIconComponent,
TuiAppearance,
TuiIcon,
TuiSkeleton,
TuiDrawer,
TuiPopup,
i18nPipe,
DocsLinkDirective,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class MenuComponent implements OnDestroy {
@Input({ required: true })

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
import { StoreIconComponent } from './store-icon.component'
import { StoreIconComponentModule } from './store-icon/store-icon.component.module'
@Component({
selector: '[registry]',
@@ -17,7 +17,7 @@ import { StoreIconComponent } from './store-icon.component'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [StoreIconComponent, TuiIcon, TuiTitle],
imports: [StoreIconComponentModule, TuiIcon, TuiTitle],
})
export class MarketplaceRegistryComponent {
@Input()

View File

@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { StoreIconComponent } from './store-icon.component'
@NgModule({
declarations: [StoreIconComponent],
imports: [CommonModule],
exports: [StoreIconComponent],
})
export class StoreIconComponentModule {}

View File

@@ -21,6 +21,7 @@ import { knownRegistries, sameUrl } from '@start9labs/shared'
`,
styles: ':host { overflow: hidden; }',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class StoreIconComponent {
@Input()

View File

@@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -6,11 +5,7 @@ import {
Input,
Output,
} from '@angular/core'
import { RouterModule } from '@angular/router'
import { LocalizePipe } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { TuiAppearance, TuiIcon } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
const ICONS: Record<string, string> = {
all: '@tui.layout-grid',
@@ -31,15 +26,8 @@ const ICONS: Record<string, string> = {
selector: 'marketplace-categories',
templateUrl: 'categories.component.html',
styleUrls: ['categories.component.scss'],
imports: [
RouterModule,
CommonModule,
TuiAppearance,
TuiIcon,
TuiSkeleton,
LocalizePipe,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class CategoriesComponent {
@Input()

View File

@@ -0,0 +1,15 @@
import { TuiIcon, TuiAppearance } from '@taiga-ui/core'
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { LocalizePipe } from '@start9labs/shared'
import { CategoriesComponent } from './categories.component'
import { RouterModule } from '@angular/router'
@NgModule({
imports: [RouterModule, CommonModule, TuiAppearance, TuiIcon, TuiSkeleton, LocalizePipe],
declarations: [CategoriesComponent],
exports: [CategoriesComponent],
})
export class CategoriesModule {}

View File

@@ -1,15 +1,12 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterModule } from '@angular/router'
import { LocalizePipe, TickerComponent } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types'
@Component({
selector: 'marketplace-item',
templateUrl: 'item.component.html',
styleUrls: ['item.component.scss'],
imports: [CommonModule, RouterModule, TickerComponent, LocalizePipe],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class ItemComponent {
@Input({ required: true })

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared'
import { ItemComponent } from './item.component'
@NgModule({
declarations: [ItemComponent],
exports: [ItemComponent],
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe],
})
export class ItemModule {}

View File

@@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -6,15 +5,13 @@ import {
Input,
Output,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiIcon } from '@taiga-ui/core'
@Component({
selector: 'marketplace-search',
templateUrl: 'search.component.html',
styleUrls: ['search.component.scss'],
imports: [FormsModule, CommonModule, TuiIcon],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class SearchComponent {
@Input()

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiIcon } from '@taiga-ui/core'
import { SearchComponent } from './search.component'
@NgModule({
imports: [FormsModule, CommonModule, TuiIcon],
declarations: [SearchComponent],
exports: [SearchComponent],
})
export class SearchModule {}

View File

@@ -1,12 +1,7 @@
import { KeyValue } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core'
import { RouterModule } from '@angular/router'
import { i18nPipe, i18nService } from '@start9labs/shared'
import { ExverPipesModule, i18nPipe, i18nService } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
import { MarketplacePkgBase } from '../../../types'
@@ -25,7 +20,9 @@ import { MarketplacePkgBase } from '../../../types'
<tui-line-clamp [linesLimit]="2" [content]="titleContent" />
<ng-template #titleContent>
<div class="title">
<span>{{ getTitle(dep.key) }}</span>
<span>
{{ getTitle(dep.key) }}
</span>
<p>
@if (dep.value.optional) {
<span>({{ 'Optional' | i18n }})</span>
@@ -40,7 +37,9 @@ import { MarketplacePkgBase } from '../../../types'
[content]="descContent"
class="description"
/>
<ng-template #descContent>{{ dep.value.description }}</ng-template>
<ng-template #descContent>
{{ dep.value.description }}
</ng-template>
</div>
</div>
`,
@@ -95,7 +94,7 @@ import { MarketplacePkgBase } from '../../../types'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterModule, TuiAvatar, TuiLineClamp, i18nPipe],
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp, i18nPipe],
})
export class MarketplaceDepItemComponent {
private readonly i18nService = inject(i18nService)

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterLink } from '@angular/router'
import { i18nPipe, TrustUrlPipe } from '@start9labs/shared'
import { i18nPipe, SharedPipesModule } from '@start9labs/shared'
import { TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout'
@@ -47,7 +47,14 @@ import { MarketplacePkg } from '../../types'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink, TuiCell, TuiTitle, TrustUrlPipe, TuiAvatar, i18nPipe],
imports: [
RouterLink,
TuiCell,
TuiTitle,
SharedPipesModule,
TuiAvatar,
i18nPipe,
],
})
export class MarketplaceFlavorsComponent {
@Input()

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TickerComponent } from '@start9labs/shared'
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
@Component({
@@ -118,7 +118,7 @@ import { T } from '@start9labs/start-sdk'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TickerComponent],
imports: [SharedPipesModule, TickerComponent],
})
export class MarketplacePackageHeroComponent {
@Input({ required: true })

View File

@@ -7,7 +7,7 @@ import {
TemplateRef,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { DialogService, i18nPipe } from '@start9labs/shared'
import { DialogService, i18nPipe, SharedPipesModule } from '@start9labs/shared'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { TuiRadioList } from '@taiga-ui/kit'
import { filter } from 'rxjs'
@@ -76,6 +76,7 @@ import { MarketplaceItemComponent } from './item.component'
imports: [
MarketplaceItemComponent,
TuiButton,
SharedPipesModule,
FormsModule,
TuiRadioList,
i18nPipe,

View File

@@ -4,6 +4,7 @@ import Fuse from 'fuse.js'
@Pipe({
name: 'filterPackages',
standalone: false,
})
export class FilterPackagesPipe implements PipeTransform {
transform(
@@ -78,3 +79,9 @@ export class FilterPackagesPipe implements PipeTransform {
.map(a => ({ ...a }))
}
}
@NgModule({
declarations: [FilterPackagesPipe],
exports: [FilterPackagesPipe],
})
export class FilterPackagesPipeModule {}

View File

@@ -3,8 +3,11 @@
*/
export * from './pages/list/categories/categories.component'
export * from './pages/list/categories/categories.module'
export * from './pages/list/item/item.component'
export * from './pages/list/item/item.module'
export * from './pages/list/search/search.component'
export * from './pages/list/search/search.module'
export * from './pages/show/link.component'
export * from './pages/show/item.component'
export * from './pages/show/links.component'
@@ -19,7 +22,10 @@ export * from './pages/show/release-notes.component'
export * from './pipes/filter-packages.pipe'
export * from './components/store-icon.component'
export * from './components/store-icon/store-icon.component'
export * from './components/store-icon/store-icon.component.module'
export * from './components/store-icon/store-icon.component'
export * from './components/menu/menu.component.module'
export * from './components/menu/menu.component'
export * from './components/registry.component'

View File

@@ -1,17 +1,15 @@
import { Component, DOCUMENT, inject, OnInit } from '@angular/core'
import { Router, RouterOutlet } from '@angular/router'
import { Component, inject, DOCUMENT } from '@angular/core'
import { Router } from '@angular/router'
import { ErrorService } from '@start9labs/shared'
import { TuiRoot } from '@taiga-ui/core'
import { ApiService } from './services/api.service'
import { StateService } from './services/state.service'
@Component({
selector: 'app-root',
template: '<tui-root tuiTheme="dark"><router-outlet /></tui-root>',
imports: [TuiRoot, RouterOutlet],
standalone: false,
})
export class AppComponent implements OnInit {
export class AppComponent {
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
private readonly router = inject(Router)

View File

@@ -3,20 +3,9 @@ import {
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import {
ApplicationConfig,
inject,
provideAppInitializer,
provideZoneChangeDetection,
signal,
} from '@angular/core'
import { provideAnimations } from '@angular/platform-browser/animations'
import {
PreloadAllModules,
provideRouter,
withDisabledInitialNavigation,
withPreloading,
} from '@angular/router'
import { inject, NgModule, provideAppInitializer } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { PreloadAllModules, RouterModule } from '@angular/router'
import { WA_LOCATION } from '@ng-web-apis/common'
import initArgon from '@start9labs/argon2'
import {
@@ -26,16 +15,13 @@ import {
VERSION,
WorkspaceConfig,
} from '@start9labs/shared'
import {
tuiButtonOptionsProvider,
tuiTextfieldOptionsProvider,
} from '@taiga-ui/core'
import { provideEventPlugins } from '@taiga-ui/event-plugins'
import { ROUTES } from './app.routes'
import { tuiButtonOptionsProvider, TuiRoot } from '@taiga-ui/core'
import { NG_EVENT_PLUGINS } from '@taiga-ui/event-plugins'
import { ApiService } from './services/api.service'
import { LiveApiService } from './services/live-api.service'
import { MockApiService } from './services/mock-api.service'
import { AppComponent } from './app.component'
import { ROUTES } from './app.routes'
const {
useMocks,
@@ -44,16 +30,18 @@ const {
const version = require('../../../../package.json').version
export const APP_CONFIG: ApplicationConfig = {
@NgModule({
declarations: [AppComponent],
imports: [
BrowserAnimationsModule,
RouterModule.forRoot(ROUTES, {
preloadingStrategy: PreloadAllModules,
initialNavigation: 'disabled',
}),
TuiRoot,
],
providers: [
provideZoneChangeDetection(),
provideAnimations(),
provideEventPlugins(),
provideRouter(
ROUTES,
withDisabledInitialNavigation(),
withPreloading(PreloadAllModules),
),
NG_EVENT_PLUGINS,
I18N_PROVIDERS,
provideSetupLogsService(ApiService),
tuiButtonOptionsProvider({ size: 'm' }),
@@ -76,6 +64,7 @@ export const APP_CONFIG: ApplicationConfig = {
initArgon({ module_or_path })
}),
tuiTextfieldOptionsProvider({ cleaner: signal(false) }),
],
}
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -5,6 +5,7 @@ import { TuiDialogContext } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
standalone: true,
imports: [TuiButton, i18nPipe],
template: `
<p>{{ 'This drive contains existing StartOS data.' | i18n }}</p>

View File

@@ -4,6 +4,7 @@ import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
standalone: true,
imports: [TuiButton, i18nPipe],
template: `
<div class="animation-container">

View File

@@ -11,6 +11,7 @@ interface Data {
}
@Component({
standalone: true,
imports: [FormsModule, TuiTextfield, TuiSelect, TuiDataListWrapper, i18nPipe],
template: `
<p>{{ 'Multiple backups found. Select which one to restore.' | i18n }}</p>

View File

@@ -11,6 +11,7 @@ import { TuiPassword } from '@taiga-ui/kit'
import { injectContext } from '@taiga-ui/polymorpheus'
@Component({
standalone: true,
imports: [
FormsModule,
TuiButton,

View File

@@ -286,7 +286,7 @@ export default class SuccessPage implements AfterViewInit {
while (attempts < maxAttempts) {
try {
await this.api.echo({ message: 'ping' }, `${this.lanAddress}/rpc/v1`)
await this.api.echo({ message: 'ping' }, this.lanAddress)
return
} catch {
await new Promise(resolve => setTimeout(resolve, 5000))

View File

@@ -1,11 +1,13 @@
import { enableProdMode } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from 'src/app/app.component'
import { APP_CONFIG } from 'src/app/app.config'
import { environment } from 'src/environments/environment'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
import { environment } from './environments/environment'
if (environment.production) {
enableProdMode()
}
bootstrapApplication(AppComponent, APP_CONFIG).catch(console.error)
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err))

View File

@@ -1,22 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
// converts bytes to gigabytes
@Pipe({
name: 'convertBytes',
})
export class ConvertBytesPipe implements PipeTransform {
transform(bytes: number): string {
return convertBytes(bytes)
}
}
export function convertBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { ExverComparesPipe, ExverSatisfiesPipe } from './exver.pipe'
@NgModule({
declarations: [ExverComparesPipe, ExverSatisfiesPipe],
exports: [ExverComparesPipe, ExverSatisfiesPipe],
})
export class ExverPipesModule {}

View File

@@ -1,11 +1,28 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '../services/exver.service'
import { Pipe, PipeTransform } from '@angular/core'
import { Exver } from '../../services/exver.service'
@Pipe({
name: 'satisfiesExver',
standalone: false,
})
export class ExverSatisfiesPipe implements PipeTransform {
constructor(private readonly exver: Exver) {}
transform(versionUnderTest?: string, range?: string): boolean {
return (
!!versionUnderTest &&
!!range &&
this.exver.satisfies(versionUnderTest, range)
)
}
}
@Pipe({
name: 'compareExver',
standalone: false,
})
export class ExverComparesPipe implements PipeTransform {
private readonly exver = inject(Exver)
constructor(private readonly exver: Exver) {}
transform(first: string, second: string): SemverResult {
try {

View File

@@ -1,11 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'
import { isEmptyObject } from '../util/misc.util'
import { isEmptyObject } from '../../util/misc.util'
@Pipe({
name: 'empty',
standalone: false,
})
export class EmptyPipe implements PipeTransform {
transform(val: object | [] = {}): boolean {
return Array.isArray(val) ? !val.length : isEmptyObject(val)
if (Array.isArray(val)) return !val.length
return isEmptyObject(val)
}
}

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({
name: 'includes',
standalone: false,
})
export class IncludesPipe implements PipeTransform {
transform<T>(list: T[], val: T): boolean {
return list.includes(val)
}
}

View File

@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core'
import { IncludesPipe } from './includes.pipe'
import { EmptyPipe } from './empty.pipe'
import { TrustUrlPipe } from './trust.pipe'
@NgModule({
declarations: [IncludesPipe, EmptyPipe, TrustUrlPipe],
exports: [IncludesPipe, EmptyPipe, TrustUrlPipe],
})
export class SharedPipesModule {}

View File

@@ -0,0 +1,35 @@
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({
name: 'sort',
standalone: false,
})
export class SortPipe implements PipeTransform {
transform(
value: any[],
column: string = '',
direction: string = 'asc',
): any[] {
// If the value is not an array or is empty, return the original value
if (!Array.isArray(value) || value.length === 0) {
return value
}
// Clone the array to avoid modifying the original value
const sortedValue = [...value]
// Define the sorting function based on the column and direction parameters
const sortingFn = (a: any, b: any): number => {
if (a[column] < b[column]) {
return direction === 'asc' ? -1 : 1
} else if (a[column] > b[column]) {
return direction === 'asc' ? 1 : -1
} else {
return 0
}
}
// Sort the array and return the result
return sortedValue.sort(sortingFn)
}
}

View File

@@ -1,11 +1,12 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Pipe, PipeTransform } from '@angular/core'
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
@Pipe({
name: 'trustUrl',
standalone: false,
})
export class TrustUrlPipe implements PipeTransform {
private readonly sanitizer = inject(DomSanitizer)
constructor(private readonly sanitizer: DomSanitizer) {}
transform(base64Icon: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(base64Icon)

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { ConvertBytesPipe, DurationToSecondsPipe } from './unit-conversion.pipe'
@NgModule({
declarations: [ConvertBytesPipe, DurationToSecondsPipe],
exports: [ConvertBytesPipe, DurationToSecondsPipe],
})
export class UnitConversionPipesModule {}

View File

@@ -0,0 +1,49 @@
import { Pipe, PipeTransform } from '@angular/core'
// converts bytes to gigabytes
@Pipe({
name: 'convertBytes',
standalone: false,
})
export class ConvertBytesPipe implements PipeTransform {
transform(bytes: number): string {
return convertBytes(bytes)
}
}
export function convertBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
@Pipe({
name: 'durationToSeconds',
standalone: false,
})
export class DurationToSecondsPipe implements PipeTransform {
transform(duration?: string | null): number {
if (!duration) return 0
const regex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
const [, num, , unit] = duration.match(regex) || []
const multiplier = (unit && unitsToSeconds[unit]) || NaN
return unit ? Number(num) * multiplier : NaN
}
}
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const unitsToSeconds: Record<string, number> = {
ns: 1e-9,
µs: 1e-6,
ms: 0.001,
s: 1,
m: 60,
h: 3600,
d: 86400,
}

View File

@@ -20,10 +20,14 @@ export * from './i18n/i18n.providers'
export * from './i18n/i18n.service'
export * from './i18n/localize.pipe'
export * from './pipes/exver-compares.pipe'
export * from './pipes/empty.pipe'
export * from './pipes/trust.pipe'
export * from './pipes/convert-bytes.pipe'
export * from './pipes/exver/exver.module'
export * from './pipes/exver/exver.pipe'
export * from './pipes/shared/shared.module'
export * from './pipes/shared/empty.pipe'
export * from './pipes/shared/includes.pipe'
export * from './pipes/shared/trust.pipe'
export * from './pipes/unit-conversion/unit-conversion.module'
export * from './pipes/unit-conversion/unit-conversion.pipe'
export * from './pipes/markdown.pipe'
export * from './services/copy.service'

View File

@@ -1,18 +1,14 @@
import { Component, inject } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { RouterOutlet } from '@angular/router'
import { i18nService } from '@start9labs/shared'
import { TuiRoot } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { merge } from 'rxjs'
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
import { PatchDataService } from './services/patch-data.service'
import { DataModel } from './services/patch-db/data-model'
import { PatchMonitorService } from './services/patch-monitor.service'
@Component({
selector: 'app-root',
imports: [TuiRoot, RouterOutlet, ToastContainerComponent],
template: `
<tui-root tuiTheme="dark">
<router-outlet />
@@ -30,6 +26,7 @@ import { PatchMonitorService } from './services/patch-monitor.service'
font-family: 'Proxima Nova', system-ui;
}
`,
standalone: false,
})
export class AppComponent {
private readonly i18n = inject(i18nService)

View File

@@ -1,199 +0,0 @@
import {
provideHttpClient,
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import {
ApplicationConfig,
inject,
provideAppInitializer,
provideZoneChangeDetection,
} from '@angular/core'
import { UntypedFormBuilder } from '@angular/forms'
import { provideAnimations } from '@angular/platform-browser/animations'
import {
ActivationStart,
PreloadAllModules,
provideRouter,
Router,
withComponentInputBinding,
withDisabledInitialNavigation,
withInMemoryScrolling,
withPreloading,
withRouterConfig,
} from '@angular/router'
import { provideServiceWorker } from '@angular/service-worker'
import { WA_LOCATION } from '@ng-web-apis/common'
import initArgon from '@start9labs/argon2'
import {
AbstractCategoryService,
FilterPackagesPipe,
} from '@start9labs/marketplace'
import {
I18N_PROVIDERS,
I18N_STORAGE,
i18nService,
Languages,
RELATIVE_URL,
VERSION,
WorkspaceConfig,
} from '@start9labs/shared'
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
import {
TUI_DATE_FORMAT,
TUI_DIALOGS_CLOSE,
TUI_MEDIA,
tuiAlertOptionsProvider,
tuiButtonOptionsProvider,
tuiDropdownOptionsProvider,
tuiNumberFormatProvider,
} from '@taiga-ui/core'
import { provideEventPlugins } from '@taiga-ui/event-plugins'
import {
TUI_DATE_TIME_VALUE_TRANSFORMER,
TUI_DATE_VALUE_TRANSFORMER,
} from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'
import { filter, identity, merge, of, pairwise } from 'rxjs'
import { FilterUpdatesPipe } from 'src/app/routes/portal/routes/updates/filter-updates.pipe'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { LiveApiService } from 'src/app/services/api/embassy-live-api.service'
import { MockApiService } from 'src/app/services/api/embassy-mock-api.service'
import { AuthService } from 'src/app/services/auth.service'
import { CategoryService } from 'src/app/services/category.service'
import { ClientStorageService } from 'src/app/services/client-storage.service'
import { ConfigService } from 'src/app/services/config.service'
import { DateTransformerService } from 'src/app/services/date-transformer.service'
import { DatetimeTransformerService } from 'src/app/services/datetime-transformer.service'
import {
PATCH_CACHE,
PatchDbSource,
} from 'src/app/services/patch-db/patch-db-source'
import { StateService } from 'src/app/services/state.service'
import { StorageService } from 'src/app/services/storage.service'
import { environment } from 'src/environments/environment'
import { ROUTES } from './app.routes'
const {
useMocks,
ui: { api },
} = require('../../../../config.json') as WorkspaceConfig
export const APP_CONFIG: ApplicationConfig = {
providers: [
provideZoneChangeDetection(),
provideAnimations(),
provideEventPlugins(),
provideHttpClient(withInterceptorsFromDi(), withFetch()),
provideRouter(
ROUTES,
withDisabledInitialNavigation(),
withComponentInputBinding(),
withPreloading(PreloadAllModules),
withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }),
withRouterConfig({ paramsInheritanceStrategy: 'always' }),
),
provideServiceWorker('ngsw-worker.js', {
enabled: environment.useServiceWorker,
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000',
}),
I18N_PROVIDERS,
FilterPackagesPipe,
FilterUpdatesPipe,
UntypedFormBuilder,
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
tuiButtonOptionsProvider({ size: 'm' }),
tuiDropdownOptionsProvider({ appearance: 'start-os' }),
tuiAlertOptionsProvider({
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
}),
{
provide: TUI_DATE_FORMAT,
useValue: of({
mode: 'MDY',
separator: '/',
}),
},
{
provide: TUI_DATE_VALUE_TRANSFORMER,
useClass: DateTransformerService,
},
{
provide: TUI_DATE_TIME_VALUE_TRANSFORMER,
useClass: DatetimeTransformerService,
},
{
provide: ApiService,
useClass: useMocks ? MockApiService : LiveApiService,
},
{
provide: PatchDB,
deps: [PatchDbSource, PATCH_CACHE],
useClass: PatchDB,
},
provideAppInitializer(() => {
const i18n = inject(i18nService)
const origin = inject(WA_LOCATION).origin
const module_or_path = new URL('/assets/argon2_bg.wasm', origin)
initArgon({ module_or_path })
inject(StorageService).migrate036()
inject(AuthService).init()
inject(ClientStorageService).init()
inject(Router).initialNavigation()
i18n.setLanguage(i18n.language || 'english')
}),
{
provide: RELATIVE_URL,
useValue: `/${api.url}/${api.version}`,
},
{
provide: AbstractCategoryService,
useClass: CategoryService,
},
{
provide: TUI_DIALOGS_CLOSE,
useFactory: () =>
merge(
inject(Router).events.pipe(filter(e => e instanceof ActivationStart)),
inject(StateService).pipe(
pairwise(),
filter(
([prev, curr]) =>
prev === 'running' &&
(curr === 'error' || curr === 'initializing'),
),
),
),
},
{
provide: I18N_STORAGE,
useFactory: () => {
const api = inject(ApiService)
return (language: Languages) => api.setLanguage({ language })
},
},
{
provide: VERSION,
useFactory: () => inject(ConfigService).version,
},
tuiObfuscateOptionsProvider({
recipes: {
mask: ({ length }) => '•'.repeat(length),
none: identity,
},
}),
{
provide: TUI_MEDIA,
useValue: {
mobile: 1000,
desktopSmall: 1280,
desktopLarge: Infinity,
},
},
],
}

View File

@@ -0,0 +1,36 @@
import {
provideHttpClient,
withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { ServiceWorkerModule } from '@angular/service-worker'
import { TuiRoot } from '@taiga-ui/core'
import { ToastContainerComponent } from 'src/app/components/toast-container.component'
import { environment } from '../environments/environment'
import { AppComponent } from './app.component'
import { APP_PROVIDERS } from './app.providers'
import { RoutingModule } from './routing.module'
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
RoutingModule,
ToastContainerComponent,
TuiRoot,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.useServiceWorker,
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000',
}),
],
providers: [
APP_PROVIDERS,
provideHttpClient(withInterceptorsFromDi(), withFetch()),
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,157 @@
import { inject, provideAppInitializer } from '@angular/core'
import { UntypedFormBuilder } from '@angular/forms'
import { provideAnimations } from '@angular/platform-browser/animations'
import { ActivationStart, Router } from '@angular/router'
import { WA_LOCATION } from '@ng-web-apis/common'
import initArgon from '@start9labs/argon2'
import {
AbstractCategoryService,
FilterPackagesPipe,
} from '@start9labs/marketplace'
import {
I18N_PROVIDERS,
I18N_STORAGE,
i18nService,
Languages,
RELATIVE_URL,
VERSION,
WorkspaceConfig,
} from '@start9labs/shared'
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
import {
TUI_DATE_FORMAT,
TUI_DIALOGS_CLOSE,
TUI_MEDIA,
tuiAlertOptionsProvider,
tuiButtonOptionsProvider,
tuiDropdownOptionsProvider,
tuiNumberFormatProvider,
} from '@taiga-ui/core'
import { provideEventPlugins } from '@taiga-ui/event-plugins'
import {
TUI_DATE_TIME_VALUE_TRANSFORMER,
TUI_DATE_VALUE_TRANSFORMER,
} from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'
import { filter, identity, merge, of, pairwise } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import {
PATCH_CACHE,
PatchDbSource,
} from 'src/app/services/patch-db/patch-db-source'
import { StateService } from 'src/app/services/state.service'
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
import { ApiService } from './services/api/embassy-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { AuthService } from './services/auth.service'
import { CategoryService } from './services/category.service'
import { ClientStorageService } from './services/client-storage.service'
import { DateTransformerService } from './services/date-transformer.service'
import { DatetimeTransformerService } from './services/datetime-transformer.service'
import { StorageService } from './services/storage.service'
const {
useMocks,
ui: { api },
} = require('../../../../config.json') as WorkspaceConfig
export const APP_PROVIDERS = [
provideAnimations(),
provideEventPlugins(),
I18N_PROVIDERS,
FilterPackagesPipe,
FilterUpdatesPipe,
UntypedFormBuilder,
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
tuiButtonOptionsProvider({ size: 'm' }),
tuiDropdownOptionsProvider({ appearance: 'start-os' }),
tuiAlertOptionsProvider({
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
}),
{
provide: TUI_DATE_FORMAT,
useValue: of({
mode: 'MDY',
separator: '/',
}),
},
{
provide: TUI_DATE_VALUE_TRANSFORMER,
useClass: DateTransformerService,
},
{
provide: TUI_DATE_TIME_VALUE_TRANSFORMER,
useClass: DatetimeTransformerService,
},
{
provide: ApiService,
useClass: useMocks ? MockApiService : LiveApiService,
},
{
provide: PatchDB,
deps: [PatchDbSource, PATCH_CACHE],
useClass: PatchDB,
},
provideAppInitializer(() => {
const i18n = inject(i18nService)
const origin = inject(WA_LOCATION).origin
const module_or_path = new URL('/assets/argon2_bg.wasm', origin)
initArgon({ module_or_path })
inject(StorageService).migrate036()
inject(AuthService).init()
inject(ClientStorageService).init()
inject(Router).initialNavigation()
i18n.setLanguage(i18n.language || 'english')
}),
{
provide: RELATIVE_URL,
useValue: `/${api.url}/${api.version}`,
},
{
provide: AbstractCategoryService,
useClass: CategoryService,
},
{
provide: TUI_DIALOGS_CLOSE,
useFactory: () =>
merge(
inject(Router).events.pipe(filter(e => e instanceof ActivationStart)),
inject(StateService).pipe(
pairwise(),
filter(
([prev, curr]) =>
prev === 'running' &&
(curr === 'error' || curr === 'initializing'),
),
),
),
},
{
provide: I18N_STORAGE,
useFactory: () => {
const api = inject(ApiService)
return (language: Languages) => api.setLanguage({ language })
},
},
{
provide: VERSION,
useFactory: () => inject(ConfigService).version,
},
tuiObfuscateOptionsProvider({
recipes: {
mask: ({ length }) => '•'.repeat(length),
none: identity,
},
}),
{
provide: TUI_MEDIA,
useValue: {
mobile: 1000,
desktopSmall: 1280,
desktopLarge: Infinity,
},
},
]

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
const ROUTES: Routes = [
{
path: '',
loadChildren: () =>
import('./home/home.module').then(m => m.HomePageModule),
},
{
path: 'logs',
loadComponent: () => import('./logs.component'),
},
]
@NgModule({
imports: [RouterModule.forChild(ROUTES)],
})
export default class DiagnosticModule {}

View File

@@ -1,12 +0,0 @@
import { Routes } from '@angular/router'
export default [
{
path: '',
loadComponent: () => import('./home/home.page'),
},
{
path: 'logs',
loadComponent: () => import('./logs.component'),
},
] satisfies Routes

View File

@@ -0,0 +1,19 @@
import { TuiButton } from '@taiga-ui/core'
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { RouterModule, Routes } from '@angular/router'
import { HomePage } from './home.page'
import { i18nPipe } from '@start9labs/shared'
const ROUTES: Routes = [
{
path: '',
component: HomePage,
},
]
@NgModule({
imports: [CommonModule, TuiButton, RouterModule.forChild(ROUTES), i18nPipe],
declarations: [HomePage],
})
export class HomePageModule {}

View File

@@ -1,14 +1,6 @@
import { CommonModule } from '@angular/common'
import { Component, Inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { WA_WINDOW } from '@ng-web-apis/common'
import {
DialogService,
i18nKey,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core'
import { DialogService, i18nKey, LoadingService } from '@start9labs/shared'
import { filter } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
@@ -17,9 +9,9 @@ import { ConfigService } from 'src/app/services/config.service'
selector: 'diagnostic-home',
templateUrl: 'home.component.html',
styleUrls: ['home.page.scss'],
imports: [CommonModule, TuiButton, i18nPipe, RouterLink],
standalone: false,
})
export default class HomePage {
export class HomePage {
restarted = false
error?: {
code: number

View File

@@ -20,7 +20,9 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { StateService } from 'src/app/services/state.service'
@Component({
template: '<app-initializing [progress]="progress()" />',
template: `
<app-initializing [progress]="progress()" />
`,
providers: [provideSetupLogsService(ApiService)],
styles: ':host { height: 100%; }',
imports: [InitializingComponent],

View File

@@ -0,0 +1,37 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { RouterModule, Routes } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiButton, TuiError, TuiIcon, TuiTextfield } from '@taiga-ui/core'
import { TuiPassword } from '@taiga-ui/kit'
import { TuiCardLarge } from '@taiga-ui/layout'
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
import { LoginPage } from './login.page'
const routes: Routes = [
{
path: '',
component: LoginPage,
},
]
@NgModule({
imports: [
CommonModule,
FormsModule,
CAWizardComponent,
TuiButton,
TuiCardLarge,
...TuiTextfield,
TuiIcon,
TuiPassword,
TuiAutoFocus,
TuiError,
RouterModule.forChild(routes),
i18nPipe,
],
declarations: [LoginPage],
})
export class LoginPageModule {}

View File

@@ -1,38 +1,19 @@
import { CommonModule } from '@angular/common'
import { Component, DestroyRef, DOCUMENT, inject, Inject } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import { i18nKey, i18nPipe, LoadingService } from '@start9labs/shared'
import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiButton, TuiError, TuiIcon, TuiTextfield } from '@taiga-ui/core'
import { TuiPassword } from '@taiga-ui/kit'
import { TuiCardLarge } from '@taiga-ui/layout'
import { CAWizardComponent } from 'src/app/routes/login/ca-wizard/ca-wizard.component'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { Component, Inject, DestroyRef, inject, DOCUMENT } from '@angular/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { AuthService } from 'src/app/services/auth.service'
import { ConfigService } from 'src/app/services/config.service'
import { i18nKey, LoadingService } from '@start9labs/shared'
@Component({
selector: 'login',
templateUrl: './login.component.html',
styleUrls: ['./login.page.scss'],
imports: [
CommonModule,
FormsModule,
CAWizardComponent,
TuiButton,
TuiCardLarge,
TuiTextfield,
TuiIcon,
TuiPassword,
TuiAutoFocus,
TuiError,
i18nPipe,
],
providers: [],
standalone: false,
})
export default class LoginPage {
export class LoginPage {
password = ''
error: i18nKey | null = null

View File

@@ -36,7 +36,6 @@ import { HeaderStatusComponent } from './status.component'
height: 2.75rem;
border-radius: var(--bumper);
margin: var(--bumper);
clip-path: inset(0 round var(--bumper));
overflow: hidden;
filter: grayscale(1) brightness(0.75);
@@ -108,8 +107,7 @@ import { HeaderStatusComponent } from './status.component'
&:has([data-status='success']) {
--status: transparent;
// "none" breaks border radius in Firefox
filter: grayscale(0.001);
filter: none;
}
}

View File

@@ -5,7 +5,7 @@ import {
Input,
Output,
} from '@angular/core'
import { ConvertBytesPipe } from '@start9labs/shared'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { UnknownDisk } from 'src/app/services/api/api.types'
@@ -109,7 +109,7 @@ import { UnknownDisk } from 'src/app/services/api/api.types'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, ConvertBytesPipe, TuiSkeleton],
imports: [TuiButton, UnitConversionPipesModule, TuiSkeleton],
})
export class BackupsPhysicalComponent {
@Input()

View File

@@ -12,7 +12,7 @@ import { MarketplacePkg } from '@start9labs/marketplace'
import {
ErrorService,
Exver,
ExverComparesPipe,
ExverPipesModule,
i18nPipe,
i18nService,
LoadingService,
@@ -107,7 +107,7 @@ type KEYS = 'id' | 'version' | 'alerts' | 'flavor'
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
ExverComparesPipe,
ExverPipesModule,
TuiButton,
ToManifestPipe,
i18nPipe,
@@ -150,11 +150,7 @@ export class MarketplaceControlsComponent {
const originalUrl = localPkg?.registry || null
if (!localPkg) {
if (
await this.alerts.alertInstall(
this.i18n.localize(this.pkg().alerts.install || ''),
)
) {
if (await this.alerts.alertInstall(this.i18n.localize(this.pkg().alerts.install || ''))) {
this.installOrUpload(currentUrl)
}
return

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { MenuComponent } from '@start9labs/marketplace'
import { MenuModule } from '@start9labs/marketplace'
import { TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core'
import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@@ -41,7 +41,7 @@ import { DialogService, i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
MenuComponent,
MenuModule,
TuiButton,
TuiIcon,
TuiAppearance,

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -7,7 +8,7 @@ import {
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, Router } from '@angular/router'
import { ItemComponent, MarketplacePkg } from '@start9labs/marketplace'
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
import { TuiAutoFocus } from '@taiga-ui/cdk'
import { TuiButton, TuiDropdownService, TuiPopup } from '@taiga-ui/core'
import { TuiDrawer } from '@taiga-ui/kit'
@@ -77,7 +78,8 @@ import { MarketplaceSidebarService } from '../services/sidebar.service'
},
],
imports: [
ItemComponent,
CommonModule,
ItemModule,
TuiAutoFocus,
TuiButton,
TuiPopup,

View File

@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'
import {
AbstractCategoryService,
FilterPackagesPipe,
FilterPackagesPipeModule,
} from '@start9labs/marketplace'
import { i18nPipe } from '@start9labs/shared'
import { TuiScrollbar } from '@taiga-ui/core'
@@ -152,7 +153,7 @@ import { ConfigService } from 'src/app/services/config.service'
MarketplaceMenuComponent,
MarketplaceNotificationComponent,
TuiScrollbar,
FilterPackagesPipe,
FilterPackagesPipeModule,
TitleDirective,
i18nPipe,
],

View File

@@ -15,7 +15,12 @@ import {
MarketplaceReleaseNotesComponent,
MarketplaceVersionsComponent,
} from '@start9labs/marketplace'
import { DialogService, EmptyPipe, Exver, MARKDOWN } from '@start9labs/shared'
import {
DialogService,
Exver,
MARKDOWN,
SharedPipesModule,
} from '@start9labs/shared'
import { TuiLoader } from '@taiga-ui/core'
import {
BehaviorSubject,
@@ -110,7 +115,7 @@ import { MarketplaceControlsComponent } from '../components/controls.component'
CommonModule,
MarketplacePackageHeroComponent,
MarketplaceDependenciesComponent,
EmptyPipe,
SharedPipesModule,
TuiLoader,
MarketplaceLinksComponent,
MarketplaceFlavorsComponent,

View File

@@ -1,7 +1,10 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { Router } from '@angular/router'
import { MarketplaceRegistryComponent } from '@start9labs/marketplace'
import {
MarketplaceRegistryComponent,
StoreIconComponentModule,
} from '@start9labs/marketplace'
import {
DialogService,
ErrorService,
@@ -11,7 +14,6 @@ import {
sameUrl,
toUrl,
} from '@start9labs/shared'
import { IST, utils } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -22,6 +24,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { IST, utils } from '@start9labs/start-sdk'
import { StorageService } from 'src/app/services/storage.service'
@Component({
@@ -77,6 +80,7 @@ import { StorageService } from 'src/app/services/storage.service'
TuiTitle,
TuiButton,
MarketplaceRegistryComponent,
StoreIconComponentModule,
i18nPipe,
],
})

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import { Component, inject, input } from '@angular/core'
import {
MarketplaceAboutComponent,
@@ -6,7 +7,7 @@ import {
MarketplacePackageHeroComponent,
MarketplaceReleaseNotesComponent,
} from '@start9labs/marketplace'
import { DialogService, EmptyPipe, MARKDOWN } from '@start9labs/shared'
import { DialogService, MARKDOWN, SharedPipesModule } from '@start9labs/shared'
import { of } from 'rxjs'
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
import { MarketplacePkgSideload } from './sideload.utils'
@@ -69,7 +70,8 @@ import { MarketplacePkgSideload } from './sideload.utils'
}
`,
imports: [
EmptyPipe,
CommonModule,
SharedPipesModule,
MarketplaceAboutComponent,
MarketplaceLinksComponent,
MarketplacePackageHeroComponent,

View File

@@ -7,7 +7,12 @@ import {
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, RouterLink } from '@angular/router'
import { DialogService, DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import {
DialogService,
DocsLinkDirective,
i18nPipe,
UnitConversionPipesModule,
} from '@start9labs/shared'
import { TuiMapperPipe } from '@taiga-ui/cdk'
import {
TuiButton,
@@ -145,6 +150,7 @@ import { BACKUP_RESTORE } from './restore.component'
TuiNotification,
TuiMapperPipe,
TitleDirective,
UnitConversionPipesModule,
BackupNetworkComponent,
BackupPhysicalComponent,
BackupProgressComponent,

View File

@@ -5,7 +5,11 @@ import {
output,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ConvertBytesPipe, DialogService, i18nPipe } from '@start9labs/shared'
import {
DialogService,
i18nPipe,
UnitConversionPipesModule,
} from '@start9labs/shared'
import { TuiButton, TuiIcon } from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
@@ -111,7 +115,7 @@ import { BackupStatusComponent } from './status.component'
TuiButton,
TuiIcon,
TuiTooltip,
ConvertBytesPipe,
UnitConversionPipesModule,
PlaceholderComponent,
BackupStatusComponent,
TableComponent,

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -93,6 +94,7 @@ const ipv6 =
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
FormGroupComponent,

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -143,6 +144,7 @@ import { GatewaysTableComponent } from './table.component'
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [GatewayService],
imports: [
CommonModule,
FormsModule,
RouterLink,
TuiButton,

View File

@@ -7,7 +7,7 @@ import {
import { toSignal } from '@angular/core/rxjs-interop'
import {
Marketplace,
StoreIconComponent,
StoreIconComponentModule,
StoreIdentity,
} from '@start9labs/marketplace'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
@@ -230,7 +230,7 @@ interface UpdatesData {
TuiBadgeNotification,
TuiFade,
TuiButton,
StoreIconComponent,
StoreIconComponentModule,
FilterUpdatesPipe,
UpdatesItemComponent,
TitleDirective,

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
import { AuthGuard } from 'src/app/guards/auth.guard'
import { UnauthGuard } from 'src/app/guards/unauth.guard'
import { stateNot } from 'src/app/services/state.service'
import { AuthGuard } from './guards/auth.guard'
import { UnauthGuard } from './guards/unauth.guard'
export const ROUTES: Routes = [
const routes: Routes = [
{
path: 'diagnostic',
canActivate: [stateNot(['initializing', 'running'])],
loadChildren: () => import('./routes/diagnostic/diagnostic.routes'),
loadChildren: () => import('./routes/diagnostic/diagnostic.module'),
},
{
path: 'initializing',
@@ -18,7 +18,8 @@ export const ROUTES: Routes = [
{
path: 'login',
canActivate: [UnauthGuard, stateNot(['error', 'initializing'])],
loadComponent: () => import('./routes/login/login.page'),
loadChildren: () =>
import('./routes/login/login.module').then(m => m.LoginPageModule),
},
{
path: '',
@@ -31,3 +32,17 @@ export const ROUTES: Routes = [
pathMatch: 'full',
},
]
@NgModule({
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
paramsInheritanceStrategy: 'always',
preloadingStrategy: PreloadAllModules,
initialNavigation: 'disabled',
bindToComponentInputs: true,
}),
],
exports: [RouterModule],
})
export class RoutingModule {}

View File

@@ -1,11 +1,12 @@
import { enableProdMode } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from 'src/app/app.component'
import { APP_CONFIG } from 'src/app/app.config'
import { environment } from 'src/environments/environment'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
import { environment } from './environments/environment'
if (environment.production) {
enableProdMode()
}
bootstrapApplication(AppComponent, APP_CONFIG).catch(console.error)
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err))

View File

@@ -19,6 +19,7 @@
"importHelpers": true,
"target": "es2022",
"module": "es2020",
"lib": ["es2020", "dom"],
"useDefineForClassFields": false,
"paths": {
/* These paths are relative to each app base folder */