Misc frontend fixes (#2974)

* fix dependency input warning and extra comma

* clean up buttons during install in marketplace preview

* chore: grayscale and closing action-bar

* fix prerelease precedence

* fix duplicate url for addSsl on ssl proto

* no warning for soft uninstall

* fix: stop logs from repeating disconnected status and add 1 second delay between reconnection attempts

* fix stop on reactivation of critical task

* fix: fix disconnected toast

* fix: updates styles

* fix: updates styles

* misc fixes

* beta.33

* fix updates badge and initialization of marketplace preview controls

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-07-08 12:08:27 -06:00
committed by GitHub
parent 340775a593
commit 7ba66c419a
32 changed files with 203 additions and 158 deletions

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.32",
"version": "0.4.0-beta.33",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -394,7 +394,9 @@ impl RpcContext {
if let Some(service) = self.services.get(&package_id).await.as_ref() {
if let Some(input) = service
.get_action_input(procedure_id.clone(), action_id.clone())
.await?
.await
.log_err()
.flatten()
.and_then(|i| i.value)
{
action_input

View File

@@ -38,7 +38,7 @@ use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
use crate::ssh::SSH_DIR;
use crate::system::{get_mem_info, sync_kiosk};
use crate::util::io::{create_file, IOHook};
use crate::util::io::{create_file, open_file, IOHook};
use crate::util::lshw::lshw;
use crate::util::net::WebSocketExt;
use crate::util::{cpupower, Invoke};
@@ -399,6 +399,11 @@ pub async fn init(
.invoke(crate::ErrorKind::Journald)
.await?;
mount_logs.complete();
tokio::io::copy(
&mut open_file("/run/startos/init.log").await?,
&mut tokio::io::stderr(),
)
.await?;
tracing::info!("Mounted Logs");
load_ca_cert.start();

View File

@@ -89,8 +89,13 @@ impl LxcManager {
log_mount: Option<&Path>,
config: LxcConfig,
) -> Result<LxcContainer, Error> {
let container = LxcContainer::new(self, log_mount, config).await?;
let mut guard = self.containers.lock().await;
let container = tokio::time::timeout(
Duration::from_secs(30),
LxcContainer::new(self, log_mount, config),
)
.await
.with_kind(ErrorKind::Timeout)??;
*guard = std::mem::take(&mut *guard)
.into_iter()
.filter(|g| g.strong_count() > 0)
@@ -223,6 +228,17 @@ impl LxcContainer {
.arg(&log_mount_point)
.invoke(crate::ErrorKind::Filesystem)
.await?;
match Command::new("chattr")
.arg("-R")
.arg("+C")
.arg(&log_mount_point)
.invoke(ErrorKind::Filesystem)
.await
{
Ok(_) => Ok(()),
Err(e) if e.source.to_string().contains("Operation not supported") => Ok(()),
Err(e) => Err(e),
}?;
Some(log_mount)
} else {
None

View File

@@ -62,12 +62,14 @@ impl BindInfo {
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
let mut assigned_port = None;
let mut assigned_ssl_port = None;
if options.secure.is_some() {
assigned_port = Some(available_ports.alloc()?);
}
if options.add_ssl.is_some() {
assigned_ssl_port = Some(available_ports.alloc()?);
}
if let Some(secure) = options.secure {
if !secure.ssl || !options.add_ssl.is_some() {
assigned_port = Some(available_ports.alloc()?);
}
}
Ok(Self {
enabled: true,
options,

View File

@@ -176,29 +176,44 @@ impl Handler<RunAction> for ServiceActor {
)
.await
.with_kind(ErrorKind::Action)?;
if self
let package_id = package_id.clone();
for to_stop in self
.0
.ctx
.db
.mutate(|db| {
let mut critical_activated = false;
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
critical_activated |= pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, package_id, action_id, &input, true))
})?;
let mut to_stop = Vec::new();
for (id, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
if pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, &package_id, action_id, &input, true))
})? {
to_stop.push(id)
}
}
Ok(critical_activated)
Ok(to_stop)
})
.await
.result?
{
<Self as Handler<super::control::Stop>>::handle(
self,
id,
super::control::Stop { wait: false },
jobs,
)
.await;
if to_stop == package_id {
<Self as Handler<super::control::Stop>>::handle(
self,
id.clone(),
super::control::Stop { wait: false },
jobs,
)
.await;
} else {
self.0
.ctx
.services
.get(&to_stop)
.await
.as_ref()
.or_not_found(&to_stop)?
.stop(id.clone(), false)
.await?;
}
}
Ok(result)
}

View File

@@ -17,9 +17,6 @@ fi
STARTOS_ENV="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/ENVIRONMENT.txt)"
PLATFORM="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/PLATFORM.txt)"
if [ -z "$1" ]; then
PLATFORM="$(uname -m)"
fi
if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then
ARCH=amd64
QEMU_ARCH=x86_64

View File

@@ -104,7 +104,11 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
this.cachedParser = built.validator
return {
spec: built.spec,
value: (await this.getInputFn(options)) || null,
value:
((await this.getInputFn(options)) as
| Record<string, unknown>
| null
| undefined) || null,
}
}
async run(options: {

View File

@@ -1,8 +1,18 @@
import { ExtendedVersion, VersionRange } from "../exver"
import { PackageId, HealthCheckId } from "../types"
import {
PackageId,
HealthCheckId,
DependencyRequirement,
CheckDependenciesResult,
} from "../types"
import { Effects } from "../Effects"
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
infoFor: (packageId: DependencyId) => {
requirement: DependencyRequirement
result: CheckDependenciesResult
}
installedSatisfied: (packageId: DependencyId) => boolean
installedVersionSatisfied: (packageId: DependencyId) => boolean
runningSatisfied: (packageId: DependencyId) => boolean
@@ -41,7 +51,7 @@ export async function checkDependencies<
)
}
const find = (packageId: DependencyId) => {
const infoFor = (packageId: DependencyId) => {
const dependencyRequirement = dependencies.find((d) => d.id === packageId)
const dependencyResult = results.find((d) => d.packageId === packageId)
if (!dependencyRequirement || !dependencyResult) {
@@ -51,9 +61,9 @@ export async function checkDependencies<
}
const installedSatisfied = (packageId: DependencyId) =>
!!find(packageId).result.installedVersion
!!infoFor(packageId).result.installedVersion
const installedVersionSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
return (
!!dep.result.installedVersion &&
ExtendedVersion.parse(dep.result.installedVersion).satisfies(
@@ -62,18 +72,18 @@ export async function checkDependencies<
)
}
const runningSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
return dep.requirement.kind !== "running" || dep.result.isRunning
}
const tasksSatisfied = (packageId: DependencyId) =>
Object.entries(find(packageId).result.tasks).filter(
Object.entries(infoFor(packageId).result.tasks).filter(
([_, t]) => t.active && t.task.severity === "critical",
).length === 0
const healthCheckSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = find(packageId)
const dep = infoFor(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
@@ -102,14 +112,14 @@ export async function checkDependencies<
: dependencies.every((d) => pkgSatisfied(d.id as DependencyId))
const throwIfInstalledNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
return null
}
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
@@ -127,14 +137,14 @@ export async function checkDependencies<
return null
}
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
throw new Error(`${dep.result.title || packageId} is not running`)
}
return null
}
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const dep = infoFor(packageId)
const reqs = Object.entries(dep.result.tasks)
.filter(([_, t]) => t.active && t.task.severity === "critical")
.map(([id, _]) => id)
@@ -149,7 +159,7 @@ export async function checkDependencies<
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = find(packageId)
const dep = infoFor(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
@@ -205,6 +215,7 @@ export async function checkDependencies<
})()
return {
infoFor,
installedSatisfied,
installedVersionSatisfied,
runningSatisfied,

View File

@@ -753,7 +753,10 @@ export class Version {
return "less"
}
const prereleaseLen = Math.max(this.number.length, other.number.length)
const prereleaseLen = Math.max(
this.prerelease.length,
other.prerelease.length,
)
for (let i = 0; i < prereleaseLen; i++) {
if (typeof this.prerelease[i] === typeof other.prerelease[i]) {
if (this.prerelease[i] > other.prerelease[i]) {

View File

@@ -180,8 +180,8 @@ export type KnownError =
export type Dependencies = Array<DependencyRequirement>
export type DeepPartial<T> = T extends unknown[]
? T
export type DeepPartial<T> = T extends [infer A, ...infer Rest]
? [DeepPartial<A>, ...DeepPartial<Rest>]
: T extends {}
? { [P in keyof T]?: DeepPartial<T[P]> }
: T

View File

@@ -26,7 +26,11 @@ export function partialDiff<T>(
} else if (typeof prev === "object" && typeof next === "object") {
if (prev === null || next === null) return { diff: next }
const res = { diff: {} as Record<keyof T, any> }
for (let key in next) {
const keys = Object.keys(next) as (keyof T)[]
for (let key in prev) {
if (!keys.includes(key)) keys.push(key)
}
for (let key of keys) {
const diff = partialDiff(prev[key], next[key])
if (diff) {
res.diff[key] = diff.diff

View File

@@ -710,7 +710,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
image,
mounts,
name,
)
).then((subc) => subc.rc())
},
/**
* @description Run a function with a temporary SubContainer

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.32",
"version": "0.4.0-beta.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.32",
"version": "0.4.0-beta.33",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.32",
"version": "0.4.0-beta.33",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",

View File

@@ -46,6 +46,7 @@ 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'
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
const {
useMocks,
@@ -56,6 +57,7 @@ export const APP_PROVIDERS = [
provideEventPlugins(),
I18N_PROVIDERS,
FilterPackagesPipe,
FilterUpdatesPipe,
UntypedFormBuilder,
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
tuiButtonOptionsProvider({ size: 'm' }),

View File

@@ -103,7 +103,6 @@ import { HeaderStatusComponent } from './status.component'
&:has([data-status='neutral']) {
--status: var(--tui-status-neutral);
filter: none;
}
&:has([data-status='success']) {

View File

@@ -18,18 +18,18 @@
@if (followLogs | logs | async; as logs) {
<section childList (waMutationObserver)="scrollToBottom()">
@for (log of logs; track log) {
@for (log of logs; track $index) {
<pre [innerHTML]="log | dompurify"></pre>
}
@if ((status$ | async) !== 'connected') {
<p class="loading-dots" [attr.data-status]="status$.value">
<div class="loading-dots" [attr.data-status]="status$.value">
{{
status$.value === 'reconnecting'
? ('Reconnecting' | i18n)
: ('Waiting for network connectivity' | i18n)
}}
</p>
</div>
}
</section>
} @else {

View File

@@ -48,4 +48,5 @@
pre {
overflow: visible;
white-space: normal;
margin: 0;
}

View File

@@ -8,16 +8,19 @@ import {
import {
bufferTime,
catchError,
concat,
defer,
delay,
EMPTY,
filter,
ignoreElements,
map,
merge,
Observable,
of,
repeat,
scan,
skipWhile,
startWith,
switchMap,
take,
tap,
@@ -62,12 +65,19 @@ export class LogsPipe implements PipeTransform {
),
).pipe(
catchError(() =>
this.connection.pipe(
tap(v => this.logs.status$.next(v ? 'reconnecting' : 'disconnected')),
filter(Boolean),
take(1),
ignoreElements(),
startWith(this.getMessage(false)),
concat(
this.logs.status$.value === 'connected'
? of(this.getMessage(false))
: EMPTY,
this.connection.pipe(
tap(v =>
this.logs.status$.next(v ? 'reconnecting' : 'disconnected'),
),
filter(Boolean),
delay(1000),
take(1),
ignoreElements(),
),
),
),
repeat(),
@@ -76,11 +86,11 @@ export class LogsPipe implements PipeTransform {
}
private getMessage(success: boolean): string {
return `<p style="color: ${
return `<div style="color: ${
success ? 'var(--tui-status-positive)' : 'var(--tui-status-negative)'
}; text-align: center;">${this.i18n.transform(
success ? 'Reconnected' : 'Disconnected',
)} at ${toLocalIsoString(new Date())}</p>`
)} at ${toLocalIsoString(new Date())}</div>`
}
private get options() {

View File

@@ -26,7 +26,7 @@ import { HeaderComponent } from './components/header/header.component'
</main>
<app-tabs />
@if (update(); as update) {
<tui-action-bar *tuiActionBar="bar">
<tui-action-bar *tuiActionBar="bar()">
@if (update === true) {
<tui-icon icon="@tui.check" class="g-positive" />
Download complete, restart to apply changes
@@ -77,8 +77,7 @@ import { HeaderComponent } from './components/header/header.component'
@include taiga.transition(filter);
header:has([data-status='success']) + &,
header:has([data-status='neutral']) + & {
header:has([data-status='success']) + & {
filter: none;
}
}
@@ -104,7 +103,7 @@ export class PortalComponent {
readonly name = toSignal(this.patch.watch$('ui', 'name'))
readonly update = toSignal(inject(OSService).updating$)
bar = true
readonly bar = signal(true)
getProgress(size: number, downloaded: number): number {
return Math.round((100 * downloaded) / (size || 1))
@@ -114,7 +113,7 @@ export class PortalComponent {
const loader = this.loader.open('Beginning restart').subscribe()
try {
this.bar = false
this.bar.set(false)
await this.api.restartServer({})
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -3,11 +3,9 @@ import {
ChangeDetectionStrategy,
Component,
inject,
Input,
input,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { Router } from '@angular/router'
import { MarketplacePkgBase } from '@start9labs/marketplace'
import {
ErrorService,
Exver,
@@ -30,21 +28,20 @@ import {
import { dryUpdate } from 'src/app/utils/dry-update'
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { MarketplacePreviewComponent } from '../modals/preview.component'
import { MarketplaceAlertsService } from '../services/alerts.service'
@Component({
selector: 'marketplace-controls',
template: `
@if (localPkg) {
@if (localPkg | toManifest; as localManifest) {
@switch (localManifest.version | compareExver: version() || '') {
@if (localPkg(); as local) {
@if (local.stateInfo.state === 'installed') {
@switch ((local | toManifest).version | compareExver: version()) {
@case (1) {
<button
tuiButton
type="button"
appearance="secondary-destructive"
appearance="warning"
(click)="tryInstall()"
>
{{ 'Downgrade' | i18n }}
@@ -81,17 +78,17 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
{{
('View' | i18n) +
' ' +
($any(localPkg.stateInfo.state | titlecase) | i18n)
($any(local.stateInfo.state | titlecase) | i18n)
}}
</button>
} @else {
<button
tuiButton
type="button"
[appearance]="localFlavor ? 'warning' : 'primary'"
appearance="primary"
(click)="tryInstall()"
>
{{ localFlavor ? ('Switch' | i18n) : ('Install' | i18n) }}
{{ localFlavor() ? ('Switch' | i18n) : ('Install' | i18n) }}
</button>
}
`,
@@ -116,29 +113,23 @@ export class MarketplaceControlsComponent {
private readonly api = inject(ApiService)
private readonly preview = inject(MarketplacePreviewComponent)
protected readonly version = toSignal(this.preview.version$)
@Input({ required: true })
pkg!: MarketplacePkgBase
@Input()
localPkg!: PackageDataEntry | null
@Input()
localFlavor!: boolean
version = input.required<string>()
installAlert = input.required<string | null>()
localPkg = input.required<PackageDataEntry | null>()
localFlavor = input.required<boolean>()
// only present if side loading
@Input()
file?: File
file = input<File>()
async tryInstall() {
const currentUrl = this.file
const localPkg = this.localPkg()
const currentUrl = this.file()
? null
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
const originalUrl = this.localPkg?.registry || null
const originalUrl = localPkg?.registry || null
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) {
if (!localPkg) {
if (await this.alerts.alertInstall(this.installAlert() || '')) {
this.installOrUpload(currentUrl)
}
return
@@ -152,12 +143,11 @@ export class MarketplaceControlsComponent {
return
}
const localManifest = getManifest(this.localPkg)
const version = this.version() || ''
const localManifest = getManifest(localPkg)
if (
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
this.exver.compareExver(localManifest.version, version) !== 0
this.exver.compareExver(localManifest.version, this.version()) !== 0
) {
this.dryInstall(currentUrl)
} else {
@@ -171,9 +161,8 @@ export class MarketplaceControlsComponent {
private async dryInstall(url: string | null) {
const id = this.preview.pkgId
const version = this.version() || ''
const breakages = dryUpdate(
{ id, version },
{ id, version: this.version() },
await getAllPackages(this.patch),
this.exver,
)
@@ -187,7 +176,7 @@ export class MarketplaceControlsComponent {
}
private async installOrUpload(url: string | null) {
if (this.file) {
if (this.file()) {
await this.upload()
this.router.navigate(['/portal', 'services'])
} else if (url) {
@@ -197,11 +186,10 @@ export class MarketplaceControlsComponent {
private async install(url: string) {
const loader = this.loader.open('Beginning install').subscribe()
const version = this.version() || ''
const id = this.preview.pkgId
try {
await this.marketplaceService.installPackage(id, version, url)
await this.marketplaceService.installPackage(id, this.version(), url)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -210,11 +198,14 @@ export class MarketplaceControlsComponent {
}
private async upload() {
const file = this.file()
if (!file) throw new Error('no file detected')
const loader = this.loader.open('Starting upload').subscribe()
try {
const { upload } = await this.api.sideloadPackage()
this.api.uploadPackage(upload, this.file!).catch(console.error)
this.api.uploadPackage(upload, file).catch(console.error)
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -56,7 +56,8 @@ import { MarketplaceControlsComponent } from './controls.component'
<marketplace-controls
slot="controls"
class="controls-wrapper"
[pkg]="pkg()"
[version]="pkg().version"
[installAlert]="pkg().alerts.install"
[localPkg]="local$ | async"
[localFlavor]="!!(flavor$ | async)"
/>

View File

@@ -62,9 +62,7 @@ export class MarketplaceAlertsService {
})
}
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
const content = alerts.install
async alertInstall(content: string): Promise<boolean> {
return (
!content ||
(!!content &&

View File

@@ -90,17 +90,20 @@ export type PackageActionData = {
`,
styles: `
tui-notification {
font-size: 1rem;
margin-bottom: 1.4rem;
margin-bottom: 1.5rem;
}
.service-title {
display: inline-flex;
align-items: center;
margin-bottom: 1.4rem;
margin-bottom: 1.5rem;
img {
height: 20px;
margin-right: 4px;
height: 1.25rem;
margin-right: 0.25rem;
border-radius: 100%;
}
h4 {
margin: 0;
}
@@ -192,7 +195,7 @@ export class ActionInputModal {
task.when?.condition === 'input-not-matches' &&
task.input &&
json
.compare(input, task.input)
.compare(input, task.input.value)
.some(op => op.op === 'add' || op.op === 'replace'),
),
)
@@ -201,9 +204,8 @@ export class ActionInputModal {
if (!breakages.length) return true
const message = `${this.i18n.transform('As a result of this change, the following services will no longer work properly and may crash')}:<ul>`
const content = `${message}${breakages.map(
id => `<li><b>${getManifest(packages[id]!).title}</b></li>`,
)}</ul>` as i18nKey
const content =
`${message}${breakages.map(id => `<li><b>${getManifest(packages[id]!).title}</b></li>`)}</ul>` as i18nKey
return firstValueFrom(
this.dialog

View File

@@ -29,7 +29,8 @@ import { MarketplacePkgSideload } from './sideload.utils'
<marketplace-controls
slot="controls"
class="controls-wrapper"
[pkg]="pkg()"
[version]="pkg().version"
[installAlert]="pkg().alerts.install"
[localPkg]="local$ | async"
[localFlavor]="!!(flavor$ | async)"
[file]="file()"

View File

@@ -1,11 +1,7 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { MarketplacePkg } from '@start9labs/marketplace'
import {
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'filterUpdates',
@@ -15,15 +11,14 @@ export class FilterUpdatesPipe implements PipeTransform {
transform(
pkgs: MarketplacePkg[],
local: Record<
string,
PackageDataEntry<InstalledState | UpdatingState>
> = {},
local: Record<string, PackageDataEntry> = {},
): MarketplacePkg[] {
return pkgs.filter(({ id, version, flavor }) => {
return pkgs.filter(({ id, flavor, version }) => {
const localPkg = local[id]
return (
localPkg &&
!!localPkg &&
(localPkg.stateInfo.state === 'installed' ||
localPkg.stateInfo.state === 'updating') &&
this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor &&
this.exver.compareExver(
version,

View File

@@ -44,7 +44,7 @@ import UpdatesComponent from './updates.component'
template: `
<tr (click)="expanded.set(!expanded())">
<td>
<div [style.gap.rem]="0.75">
<div [style.gap.rem]="0.75" [style.padding-inline-end.rem]="1">
<tui-avatar size="s"><img alt="" [src]="item().icon" /></tui-avatar>
<span tuiTitle [style.margin]="'-0.125rem 0 0'">
<b tuiFade>{{ item().title }}</b>
@@ -81,7 +81,6 @@ import UpdatesComponent from './updates.component'
</button>
@if (local().stateInfo.state === 'updating') {
<tui-progress-circle
class="g-positive"
size="xs"
[max]="100"
[value]="
@@ -175,7 +174,7 @@ import UpdatesComponent from './updates.component'
white-space: nowrap;
div {
justify-content: flex-end;
justify-content: flex-start;
}
}

View File

@@ -1,5 +1,4 @@
import { inject, Injectable } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import {
combineLatest,
@@ -19,13 +18,13 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
import { NotificationService } from 'src/app/services/notification.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { FilterUpdatesPipe } from '../routes/portal/routes/updates/filter-updates.pipe'
@Injectable({
providedIn: 'root',
})
export class BadgeService {
private readonly notifications = inject(NotificationService)
private readonly exver = inject(Exver)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly system$ = inject(OSService).updateAvailable$.pipe(
map(Number),
@@ -34,6 +33,7 @@ export class BadgeService {
.watch$('serverInfo', 'ntpSynced')
.pipe(map(synced => Number(!synced)))
private readonly marketplaceService = inject(MarketplaceService)
private readonly filterUpdatesPipe = inject(FilterUpdatesPipe)
private readonly local$ = inject(ConnectionService).pipe(
filter(Boolean),
@@ -66,17 +66,9 @@ export class BadgeService {
([marketplace, local]) =>
Object.entries(marketplace).reduce(
(list, [_, store]) =>
store?.packages.reduce(
(result, { id, version }) =>
local[id] &&
this.exver.compareExver(
version,
getManifest(local[id]!).version,
) === 1
? result.add(id)
: result,
list,
) || list,
this.filterUpdatesPipe
.transform(store?.packages || [], local)
.reduce((result, { id }) => result.add(id), list),
new Set<string>(),
).size,
),

View File

@@ -57,6 +57,10 @@ export class StandardActionsService {
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
}
if (!content) {
return this.doUninstall({ id, force, soft })
}
this.dialog
.openConfirm({
label: 'Warning',

View File

@@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core'
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
import { DialogService, i18nPipe } from '@start9labs/shared'
import { i18nPipe } from '@start9labs/shared'
import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk'
import { TuiAlertService } from '@taiga-ui/core'
import {
@@ -47,9 +47,7 @@ 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(
switchMap(() =>
@@ -101,7 +99,7 @@ export class StateService extends Observable<RR.ServerState | null> {
})
.pipe(
takeUntil(
combineLatest([this.stream$, this.network$]).pipe(
combineLatest([this.stream$.pipe(skip(1)), this.network$]).pipe(
filter(state => state.every(Boolean)),
),
),

View File

@@ -1,22 +1,16 @@
{
"/rpc/v1": {
"target": "http://<CHANGE_ME>/rpc/v1"
"target": "http://<CHANGE_ME>"
},
"/ws/*": {
"target": "http://<CHANGE_ME>",
"/ws/**": {
"target": "wss://159.223.45.210",
"secure": false,
"ws": true
},
"/public/*": {
"target": "http://<CHANGE_ME>/public",
"pathRewrite": {
"^/public": ""
}
"/public/**": {
"target": "http://<CHANGE_ME>"
},
"/rest/rpc/*": {
"target": "http://<CHANGE_ME>/rest/rpc",
"pathRewrite": {
"^/rest/rpc": ""
}
"/rest/rpc/**": {
"target": "http://<CHANGE_ME>"
}
}