From 11db199b9460e4fccd3954872d997bf18d52a3a5 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:28:56 +0700 Subject: [PATCH 1/9] Task/enforce noImplicitOverride TypeScript rule (#7355) * feat(ts): enable no implicit override * fix(api): resolve type errors * fix(client): resolve type errors * fix(ui): resolve type errors * fix(common): resolve type errors --- apps/api/src/guards/custom-throttler.guard.ts | 4 +++- .../rules/account-cluster-risk/current-investment.ts | 2 +- .../rules/account-cluster-risk/single-account.ts | 2 +- .../models/rules/asset-class-cluster-risk/equity.ts | 2 +- .../rules/asset-class-cluster-risk/fixed-income.ts | 2 +- .../base-currency-current-investment.ts | 2 +- .../currency-cluster-risk/current-investment.ts | 2 +- .../developed-markets.ts | 2 +- .../economic-market-cluster-risk/emerging-markets.ts | 2 +- .../rules/emergency-fund/emergency-fund-setup.ts | 2 +- .../rules/fees/fee-ratio-total-investment-volume.ts | 2 +- apps/api/src/models/rules/liquidity/buying-power.ts | 2 +- .../regional-market-cluster-risk/asia-pacific.ts | 2 +- .../regional-market-cluster-risk/emerging-markets.ts | 2 +- .../rules/regional-market-cluster-risk/europe.ts | 2 +- .../rules/regional-market-cluster-risk/japan.ts | 2 +- .../regional-market-cluster-risk/north-america.ts | 2 +- apps/client/src/app/adapter/custom-date-adapter.ts | 8 ++++---- .../create-asset-profile-with-market-data.dto.ts | 2 +- .../currency-selector/currency-selector.component.ts | 12 ++++++------ .../symbol-autocomplete.component.ts | 12 ++++++------ tsconfig.base.json | 2 +- 22 files changed, 37 insertions(+), 35 deletions(-) diff --git a/apps/api/src/guards/custom-throttler.guard.ts b/apps/api/src/guards/custom-throttler.guard.ts index c4f0e806d5..00a2ba087f 100644 --- a/apps/api/src/guards/custom-throttler.guard.ts +++ b/apps/api/src/guards/custom-throttler.guard.ts @@ -5,7 +5,9 @@ import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler'; export class CustomThrottlerGuard extends ThrottlerGuard { private readonly logger = new Logger(CustomThrottlerGuard.name); - public async canActivate(context: ExecutionContext): Promise { + public override async canActivate( + context: ExecutionContext + ): Promise { try { return await super.canActivate(context); } catch (error) { diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index 400a2506f6..11727e118d 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -13,7 +13,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule { private accounts: PortfolioDetails['accounts']; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, accounts: PortfolioDetails['accounts'] diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index e4ee990648..75f0503163 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -11,7 +11,7 @@ export class AccountClusterRiskSingleAccount extends Rule { private accounts: PortfolioDetails['accounts']; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, accounts: PortfolioDetails['accounts'] diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts index 12303fd927..ad5c820479 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts @@ -11,7 +11,7 @@ export class AssetClassClusterRiskEquity extends Rule { private holdings: PortfolioPosition[]; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, holdings: PortfolioPosition[] diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts index fd7c00f11b..3bc8984db3 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts @@ -11,7 +11,7 @@ export class AssetClassClusterRiskFixedIncome extends Rule { private holdings: PortfolioPosition[]; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, holdings: PortfolioPosition[] diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index 6890fecd60..96819e4d51 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -11,7 +11,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { private holdings: PortfolioPosition[]; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, holdings: PortfolioPosition[], languageCode: string diff --git a/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts index 70f09f58cd..ffab8c4c65 100644 --- a/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts +++ b/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts @@ -8,7 +8,7 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule { private developedMarketsValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, currentValueInBaseCurrency: number, developedMarketsValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts index 120c3f6a2c..6b834b52ae 100644 --- a/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts +++ b/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts @@ -8,7 +8,7 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule { private emergingMarketsValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, currentValueInBaseCurrency: number, emergingMarketsValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index fcbd99d54f..fc1d8b0c00 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -7,7 +7,7 @@ export class EmergencyFundSetup extends Rule { private emergencyFund: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, emergencyFund: number diff --git a/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts b/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts index 23f9076e85..8be31e6d4e 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts @@ -8,7 +8,7 @@ export class FeeRatioTotalInvestmentVolume extends Rule { private totalInvestmentVolumeInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, totalInvestmentVolumeInBaseCurrency: number, diff --git a/apps/api/src/models/rules/liquidity/buying-power.ts b/apps/api/src/models/rules/liquidity/buying-power.ts index 7e8b96143e..83493aabf9 100644 --- a/apps/api/src/models/rules/liquidity/buying-power.ts +++ b/apps/api/src/models/rules/liquidity/buying-power.ts @@ -7,7 +7,7 @@ export class BuyingPower extends Rule { private buyingPower: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, buyingPower: number, languageCode: string diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts b/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts index 4723389b02..bf19fc7343 100644 --- a/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts +++ b/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts @@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule { private currentValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, currentValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts index d4695406ae..7f37abecd2 100644 --- a/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts +++ b/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts @@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule { private emergingMarketsValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, currentValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/europe.ts b/apps/api/src/models/rules/regional-market-cluster-risk/europe.ts index c5cb4d134d..1e10c29bfb 100644 --- a/apps/api/src/models/rules/regional-market-cluster-risk/europe.ts +++ b/apps/api/src/models/rules/regional-market-cluster-risk/europe.ts @@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskEurope extends Rule { private europeValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, currentValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/japan.ts b/apps/api/src/models/rules/regional-market-cluster-risk/japan.ts index fc9ab92eec..22d6164450 100644 --- a/apps/api/src/models/rules/regional-market-cluster-risk/japan.ts +++ b/apps/api/src/models/rules/regional-market-cluster-risk/japan.ts @@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskJapan extends Rule { private japanValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, currentValueInBaseCurrency: number, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts b/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts index 8bd3fb0cf1..aa88dac7b1 100644 --- a/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts +++ b/apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts @@ -10,7 +10,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule { private northAmericaValueInBaseCurrency: number; public constructor( - protected exchangeRateDataService: ExchangeRateDataService, + exchangeRateDataService: ExchangeRateDataService, private i18nService: I18nService, languageCode: string, currentValueInBaseCurrency: number, diff --git a/apps/client/src/app/adapter/custom-date-adapter.ts b/apps/client/src/app/adapter/custom-date-adapter.ts index a1326b8238..5a7790b929 100644 --- a/apps/client/src/app/adapter/custom-date-adapter.ts +++ b/apps/client/src/app/adapter/custom-date-adapter.ts @@ -6,7 +6,7 @@ import { addYears, format, getYear, parse } from 'date-fns'; export class CustomDateAdapter extends NativeDateAdapter { public constructor( - @Inject(MAT_DATE_LOCALE) public locale: string, + @Inject(MAT_DATE_LOCALE) public override locale: string, @Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string ) { super(matDateLocale); @@ -15,21 +15,21 @@ export class CustomDateAdapter extends NativeDateAdapter { /** * Formats a date as a string */ - public format(aDate: Date): string { + public override format(aDate: Date): string { return format(aDate, getDateFormatString(this.locale)); } /** * Sets the first day of the week to Monday */ - public getFirstDayOfWeek(): number { + public override getFirstDayOfWeek(): number { return 1; } /** * Parses a date from a provided value */ - public parse(aValue: string): Date { + public override parse(aValue: string): Date { let date = parse(aValue, getDateFormatString(this.locale), new Date()); if (getYear(date) < 1900) { diff --git a/libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts b/libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts index 51ee716d3a..60afcb558b 100644 --- a/libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile-with-market-data.dto.ts @@ -9,7 +9,7 @@ export class CreateAssetProfileWithMarketDataDto extends CreateAssetProfileDto { @IsIn([DataSource.MANUAL], { message: `dataSource must be '${DataSource.MANUAL}'` }) - dataSource: DataSource; + override dataSource: DataSource; @IsArray() @IsOptional() diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.ts index 7b6236fbbc..724e867128 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.ts +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.ts @@ -75,22 +75,22 @@ export class GfCurrencySelectorComponent private readonly input = viewChild.required(MatInput); public constructor( - public readonly _elementRef: ElementRef, - public readonly _focusMonitor: FocusMonitor, + public override readonly _elementRef: ElementRef, + public override readonly _focusMonitor: FocusMonitor, public readonly changeDetectorRef: ChangeDetectorRef, private readonly formGroupDirective: FormGroupDirective, - public readonly ngControl: NgControl + public override readonly ngControl: NgControl ) { super(_elementRef, _focusMonitor, ngControl); this.controlType = 'currency-selector'; } - public get empty() { + public override get empty() { return this.input().empty; } - public set value(value: string | null) { + public override set value(value: string | null) { this.control.setValue(value); super.value = value; } @@ -138,7 +138,7 @@ export class GfCurrencySelectorComponent }); } - public ngDoCheck() { + public override ngDoCheck() { if (this.ngControl) { this.validateRequired(); this.errorState = !!(this.ngControl.invalid && this.ngControl.touched); diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index 4b1898b8ac..87d43d764c 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -95,22 +95,22 @@ export class GfSymbolAutocompleteComponent private readonly input = viewChild.required(MatInput); public constructor( - public readonly _elementRef: ElementRef, - public readonly _focusMonitor: FocusMonitor, + public override readonly _elementRef: ElementRef, + public override readonly _focusMonitor: FocusMonitor, public readonly changeDetectorRef: ChangeDetectorRef, public readonly dataService: DataService, - public readonly ngControl: NgControl + public override readonly ngControl: NgControl ) { super(_elementRef, _focusMonitor, ngControl); this.controlType = 'symbol-autocomplete'; } - public get empty() { + public override get empty() { return this.input().empty; } - public set value(value: LookupItem) { + public override set value(value: LookupItem) { this.control.setValue(value); super.value = value; } @@ -188,7 +188,7 @@ export class GfSymbolAutocompleteComponent }); } - public ngDoCheck() { + public override ngDoCheck() { if (this.ngControl) { this.validateRequired(); this.errorState = !!(this.ngControl.invalid && this.ngControl.touched); diff --git a/tsconfig.base.json b/tsconfig.base.json index 1c1cca5cef..f831040a02 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -30,7 +30,7 @@ "noImplicitReturns": false, "noImplicitAny": false, "noImplicitThis": true, - "noImplicitOverride": false, + "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": false, "noUnusedLocals": true, "noUnusedParameters": true, From 82faa08412b8402726348be7663255d57cedf835 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:40:05 +0200 Subject: [PATCH 2/9] Task/remove deprecated auth endpoint (#7373) * Remove deprecated auth endpoint * Update changelog --- CHANGELOG.md | 1 + README.md | 2 -- apps/api/src/app/auth/auth.controller.ts | 21 --------------------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e699e6cbb..217aba06a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`) - Simplified the `getHistorical()` function response in the data provider interface ## 3.29.0 - 2026-07-18 diff --git a/README.md b/README.md index 473d5c3679..69192124f4 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,6 @@ Set the header for each request as follows: You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ "accessToken": "" }`) -Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/` or `curl -s http://localhost:3333/api/v1/auth/anonymous/`. - ### Health Check (experimental) #### Request diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index ac50f4b8a1..e3886e39c3 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -14,7 +14,6 @@ import { Controller, Get, HttpException, - Param, Post, Req, Res, @@ -36,26 +35,6 @@ export class AuthController { private readonly webAuthService: WebAuthService ) {} - /** - * @deprecated - */ - @Get('anonymous/:accessToken') - @UseGuards(CustomThrottlerGuard) - public async accessTokenLoginGet( - @Param('accessToken') accessToken: string - ): Promise { - try { - const authToken = - await this.authService.validateAnonymousLogin(accessToken); - return { authToken }; - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - } - @Post('anonymous') @UseGuards(CustomThrottlerGuard) public async accessTokenLogin( From fbd4859d89fd196255754f3dc1491f9630a879db Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:37:38 +0200 Subject: [PATCH 3/9] Task/harmonize styling in top holdings component (#7374) Harmonize styling --- .../src/lib/top-holdings/top-holdings.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html index bb3850b8c3..e07e5fe4d9 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -72,12 +72,12 @@ - -
-
{{ parentHolding?.name }}
-
+ +
{{ parentHolding?.name }}
{{ parentHolding?.symbol | gfSymbol From 20afbdc20251ab61eece0b23737db9a3bce95e9b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:41:03 +0200 Subject: [PATCH 4/9] Task/restrict get symbol data endpoint to authenticated users (#7372) * Restrict symbol data endpoint to authenticated users * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/info/info.module.ts | 2 + apps/api/src/app/info/info.service.ts | 28 ++++++----- apps/api/src/app/symbol/symbol.controller.ts | 1 + .../home-market/home-market.component.ts | 47 +++---------------- .../components/home-market/home-market.html | 21 +-------- .../components/home-market/home-market.scss | 4 -- .../src/lib/interfaces/info-item.interface.ts | 2 +- 8 files changed, 30 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 217aba06a8..7a4a3bc176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users - Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`) - Simplified the `getHistorical()` function response in the data provider interface diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index e33c5e0c2e..06b7249095 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -7,6 +7,7 @@ import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.mo import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -29,6 +30,7 @@ import { InfoService } from './info.service'; secret: process.env.JWT_SECRET_KEY, signOptions: { expiresIn: '30 days' } }), + MarketDataModule, PlatformModule, PropertyModule, RedisCacheModule, diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 10836f7b8f..cb7d24bcba 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -1,14 +1,15 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { DEFAULT_CURRENCY, + ghostfolioFearAndGreedIndexSymbolStocks, PROPERTY_COUNTRIES_OF_SUBSCRIBERS, PROPERTY_DEMO_USER_ID, PROPERTY_DOCKER_HUB_PULLS, @@ -23,6 +24,7 @@ import { permissions } from '@ghostfolio/common/permissions'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { MarketData } from '@prisma/client'; import { subDays } from 'date-fns'; import { isNil } from 'lodash'; @@ -36,6 +38,7 @@ export class InfoService { private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly jwtService: JwtService, + private readonly marketDataService: MarketDataService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, private readonly subscriptionService: SubscriptionService, @@ -45,6 +48,7 @@ export class InfoService { public async get(): Promise { const info: Partial = {}; let isReadOnlyMode: boolean; + let latestFearAndGreedStocksMarketDataPromise: Promise; const globalPermissions: string[] = []; @@ -61,16 +65,12 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - const fearAndGreedIndexDataSource = - this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(); - - if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { - info.fearAndGreedDataSource = encodeDataSource( - fearAndGreedIndexDataSource - ); - } else { - info.fearAndGreedDataSource = fearAndGreedIndexDataSource; - } + latestFearAndGreedStocksMarketDataPromise = + this.marketDataService.getLatest({ + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), + symbol: ghostfolioFearAndGreedIndexSymbolStocks + }); globalPermissions.push(permissions.enableFearAndGreedIndex); } @@ -102,12 +102,14 @@ export class InfoService { benchmarks, demoAuthToken, isUserSignupEnabled, + latestFearAndGreedStocksMarketData, statistics, subscriptionOffer ] = await Promise.all([ this.benchmarkService.getBenchmarkAssetProfiles(), this.getDemoAuthToken(), this.propertyService.isUserSignupEnabled(), + latestFearAndGreedStocksMarketDataPromise, this.getStatistics(), this.subscriptionService.getSubscriptionOffer({ key: 'default' }) ]); @@ -125,7 +127,9 @@ export class InfoService { statistics, subscriptionOffer, baseCurrency: DEFAULT_CURRENCY, - currencies: this.exchangeRateDataService.getCurrencies() + currencies: this.exchangeRateDataService.getCurrencies(), + fearAndGreedStocksMarketPrice: + latestFearAndGreedStocksMarketData?.marketPrice }; } diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index d94ffb4dcf..a1351dbed2 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -65,6 +65,7 @@ export class SymbolController { * Must be after /lookup */ @Get(':dataSource/:symbol') + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getSymbolData( diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index 6bf99b31d1..0eec3f2d9e 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -1,16 +1,8 @@ import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config'; -import { resetHours } from '@ghostfolio/common/helper'; -import { - Benchmark, - HistoricalDataItem, - InfoItem, - User -} from '@ghostfolio/common/interfaces'; +import { Benchmark, InfoItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; -import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { DataService } from '@ghostfolio/ui/services'; import { @@ -29,11 +21,7 @@ import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - GfBenchmarkComponent, - GfFearAndGreedIndexComponent, - GfLineChartComponent - ], + imports: [GfBenchmarkComponent, GfFearAndGreedIndexComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-home-market', styleUrls: ['./home-market.scss'], @@ -41,15 +29,13 @@ import { DeviceDetectorService } from 'ngx-device-detector'; }) export class GfHomeMarketComponent implements OnInit { protected readonly benchmarks = signal([]); + protected readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); - protected readonly fearAndGreedIndex = signal(undefined); - protected readonly fearLabel = $localize`Fear`; - protected readonly greedLabel = $localize`Greed`; + + protected fearAndGreedIndex: number | undefined; protected hasPermissionToAccessFearAndGreedIndex: boolean; - protected readonly historicalDataItems = signal([]); - protected readonly numberOfDays = 365; protected user: User; private readonly info: InfoItem; @@ -80,27 +66,8 @@ export class GfHomeMarketComponent implements OnInit { permissions.enableFearAndGreedIndex ); - if ( - this.hasPermissionToAccessFearAndGreedIndex && - this.info.fearAndGreedDataSource - ) { - this.dataService - .fetchSymbolItem({ - dataSource: this.info.fearAndGreedDataSource, - includeHistoricalData: this.numberOfDays, - symbol: ghostfolioFearAndGreedIndexSymbolStocks - }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ historicalData, marketPrice }) => { - this.fearAndGreedIndex.set(marketPrice); - this.historicalDataItems.set([ - ...historicalData, - { - date: resetHours(new Date()).toISOString(), - value: marketPrice - } - ]); - }); + if (this.hasPermissionToAccessFearAndGreedIndex) { + this.fearAndGreedIndex = this.info.fearAndGreedStocksMarketPrice; } this.dataService diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index a782526eee..b1e21df955 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -1,28 +1,11 @@

Markets

@if (hasPermissionToAccessFearAndGreedIndex) { -
+
-
- Last {{ numberOfDays }} Days -
-
diff --git a/apps/client/src/app/components/home-market/home-market.scss b/apps/client/src/app/components/home-market/home-market.scss index 5b523160d5..5d4e87f30f 100644 --- a/apps/client/src/app/components/home-market/home-market.scss +++ b/apps/client/src/app/components/home-market/home-market.scss @@ -1,7 +1,3 @@ :host { display: block; - - gf-line-chart { - aspect-ratio: 16 / 9; - } } diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 01897c0664..96db7f9b06 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -9,7 +9,7 @@ export interface InfoItem { countriesOfSubscribers?: string[]; currencies: string[]; demoAuthToken: string; - fearAndGreedDataSource?: string; + fearAndGreedStocksMarketPrice?: number; globalPermissions: string[]; isDataGatheringEnabled?: string; isReadOnlyMode?: boolean; From 7cd6ebe000df672fd52c5e6bb9a3507ddbf3d882 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Jul 2026 08:20:12 +0200 Subject: [PATCH 5/9] Task/eliminate unneeded symbol pipe (#7375) Eliminate symbol pipe --- .../admin-market-data/admin-market-data.component.ts | 2 -- .../admin-market-data/admin-market-data.html | 4 +--- .../import-activities-dialog.component.ts | 2 -- .../import-activities-dialog.html | 2 +- .../allocations/allocations-page.component.ts | 8 ++++---- .../src/app/pages/public/public-page.component.ts | 8 ++++---- libs/common/src/lib/config.ts | 3 --- libs/common/src/lib/helper.ts | 5 ----- libs/common/src/lib/pipes/index.ts | 3 --- libs/common/src/lib/pipes/symbol.pipe.ts | 12 ------------ .../activities-filter.component.html | 4 ++-- .../activities-filter/activities-filter.component.ts | 2 -- .../activities-table/activities-table.component.html | 4 +--- .../activities-table.component.stories.ts | 2 -- .../activities-table/activities-table.component.ts | 2 -- .../assistant-list-item.component.ts | 3 +-- .../assistant-list-item/assistant-list-item.html | 2 +- .../portfolio-filter-form.component.html | 2 +- .../portfolio-filter-form.component.ts | 2 -- .../symbol-autocomplete.component.html | 2 +- .../symbol-autocomplete.component.ts | 2 -- .../src/lib/top-holdings/top-holdings.component.html | 2 +- .../src/lib/top-holdings/top-holdings.component.ts | 2 -- 23 files changed, 18 insertions(+), 62 deletions(-) delete mode 100644 libs/common/src/lib/pipes/index.ts delete mode 100644 libs/common/src/lib/pipes/symbol.pipe.ts diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 4b893af7b0..e716907ef3 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -13,7 +13,6 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter'; import { GfFabComponent } from '@ghostfolio/ui/fab'; import { translate } from '@ghostfolio/ui/i18n'; @@ -83,7 +82,6 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in GfActivitiesFilterComponent, GfFabComponent, GfPremiumIndicatorComponent, - GfSymbolPipe, GfValueComponent, IonIcon, MatButtonModule, diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index ebb165814c..f6744b2632 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -95,9 +95,7 @@
{{ element.name }}
@if (!isUUID(element.symbol)) {
- {{ - element.symbol | gfSymbol - }} + {{ element.symbol }}
} diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 4541009a0f..77d889b1a6 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -7,7 +7,6 @@ import { CreateTagDto } from '@ghostfolio/common/dtos'; import { Activity, PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header'; @@ -66,7 +65,6 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; GfDialogFooterComponent, GfDialogHeaderComponent, GfFileDropDirective, - GfSymbolPipe, IonIcon, MatButtonModule, MatDialogModule, diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index 4149ce5b64..559f639b15 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -51,7 +51,7 @@ >
{{ holding.assetProfile.symbol | gfSymbol }} · + >{{ holding.assetProfile.symbol }} · {{ holding.assetProfile.currency }} diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 52f051e779..de00048e7c 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -6,7 +6,7 @@ import { import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper'; +import { getCountryName } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, HoldingWithParents, @@ -495,10 +495,10 @@ export class GfAllocationsPageComponent implements OnInit { this.totalValueInEtf += this.holdings[symbol].value; } - this.symbols[prettifySymbol(symbol)] = { + this.symbols[symbol] = { + symbol, dataSource: position.assetProfile.dataSource, name: position.assetProfile.name ?? '', - symbol: prettifySymbol(symbol), value: (isNumber(position.valueInBaseCurrency) ? position.valueInBaseCurrency @@ -565,11 +565,11 @@ export class GfAllocationsPageComponent implements OnInit { return currentParentHolding && isNumber(currentParentHolding.valueInBaseCurrency) ? { + symbol, allocationInPercentage: currentParentHolding.valueInBaseCurrency / value, name: holding.assetProfile.name ?? '', position: holding, - symbol: prettifySymbol(symbol), valueInBaseCurrency: currentParentHolding.valueInBaseCurrency } diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 52d295dfa9..3b149cd086 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -1,5 +1,5 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper'; +import { getCountryName } from '@ghostfolio/common/helper'; import { InfoItem, PortfolioPosition, @@ -250,9 +250,9 @@ export class GfPublicPageComponent implements OnInit { } } - this.symbols[prettifySymbol(symbol)] = { - name: position.assetProfile.name ?? prettifySymbol(symbol), - symbol: prettifySymbol(symbol), + this.symbols[symbol] = { + symbol, + name: position.assetProfile.name ?? symbol, value: isNumber(position.valueInBaseCurrency) ? position.valueInBaseCurrency : (position.valueInPercentage ?? 0) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index f7bafb1019..a5363372a9 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -6,9 +6,6 @@ import { ColorScheme, DateRange } from './types'; export const ghostfolioPrefix = 'GF'; -/* @deprecated */ -export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; - export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = DataSource.MANUAL; export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 9a32927e23..6a135eef63 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -40,7 +40,6 @@ import { DERIVED_CURRENCIES, ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolStocks, - ghostfolioScraperApiSymbolPrefix, TAG_ID_EXCLUDE_FROM_ANALYSIS } from './config'; import { @@ -527,10 +526,6 @@ export function parseSymbol({ dataSource, symbol }: AssetProfileIdentifier) { }; } -export function prettifySymbol(aSymbol: string): string { - return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, ''); -} - export function resetHours(aDate: Date) { const year = getYear(aDate); const month = getMonth(aDate); diff --git a/libs/common/src/lib/pipes/index.ts b/libs/common/src/lib/pipes/index.ts deleted file mode 100644 index 7b5ca4bac4..0000000000 --- a/libs/common/src/lib/pipes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { GfSymbolPipe } from './symbol.pipe'; - -export { GfSymbolPipe }; diff --git a/libs/common/src/lib/pipes/symbol.pipe.ts b/libs/common/src/lib/pipes/symbol.pipe.ts deleted file mode 100644 index 6f4981699d..0000000000 --- a/libs/common/src/lib/pipes/symbol.pipe.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { prettifySymbol } from '@ghostfolio/common/helper'; - -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'gfSymbol' -}) -export class GfSymbolPipe implements PipeTransform { - public transform(aSymbol: string) { - return prettifySymbol(aSymbol); - } -} diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html index b525e51339..58626351b7 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -8,7 +8,7 @@ [removable]="true" (removed)="onRemoveFilter(filter)" > - {{ filter.label ?? '' | gfSymbol }} + {{ filter.label }} @@ -33,7 +33,7 @@ @for (filter of filterGroup.filters; track filter) { - {{ filter.label ?? '' | gfSymbol }} + {{ filter.label }} } diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts index 6b58e6aecc..7a434d94c8 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.ts +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -1,5 +1,4 @@ import { Filter, FilterGroup } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CommonModule } from '@angular/common'; @@ -39,7 +38,6 @@ import { translate } from '../i18n'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [ CommonModule, - GfSymbolPipe, IonIcon, MatAutocompleteModule, MatButtonModule, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index cc9a340475..50fb025cb4 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -172,9 +172,7 @@ !isUUID(element.assetProfile?.symbol) ) {
- {{ - element.assetProfile?.symbol | gfSymbol - }} + {{ element.assetProfile?.symbol }}
} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts index e4028e7baa..540aa92b6d 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts @@ -1,5 +1,4 @@ import { Activity } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; @@ -375,7 +374,6 @@ export default { GfActivityTypeComponent, GfEntityLogoComponent, GfNoTransactionsInfoComponent, - GfSymbolPipe, GfValueComponent, IonIcon, MatButtonModule, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index 399d5d7e19..be1f887527 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -8,7 +8,6 @@ import { Activity, AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { translate } from '@ghostfolio/ui/i18n'; import { NotificationService } from '@ghostfolio/ui/notifications'; @@ -83,7 +82,6 @@ import { GfValueComponent } from '../value/value.component'; GfActivityTypeComponent, GfEntityLogoComponent, GfNoTransactionsInfoComponent, - GfSymbolPipe, GfValueComponent, IonIcon, MatButtonModule, diff --git a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts index 36127a5669..b2c541e3f3 100644 --- a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts +++ b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.component.ts @@ -1,4 +1,3 @@ -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { FocusableOption } from '@angular/cdk/a11y'; @@ -24,7 +23,7 @@ import { @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [GfSymbolPipe, RouterModule], + imports: [RouterModule], selector: 'gf-assistant-list-item', styleUrls: ['./assistant-list-item.scss'], templateUrl: './assistant-list-item.html' diff --git a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html index fa30d0c038..45a84866bb 100644 --- a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html +++ b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html @@ -8,7 +8,7 @@ @if (item && isAsset(item)) {
{{ item?.symbol ?? '' | gfSymbol }} + >{{ item?.symbol }} @if (item.currency) { · {{ item.currency }} } diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html index 33bde3fd6c..cda9cab3c3 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -40,7 +40,7 @@ >
{{ holding.assetProfile.symbol | gfSymbol }} · + >{{ holding.assetProfile.symbol }} · {{ holding.assetProfile.currency }}
diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts index 20e8b0f0f7..9d9002c672 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts @@ -1,6 +1,5 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { AccountWithPlatform } from '@ghostfolio/common/types'; import { @@ -37,7 +36,6 @@ import { PortfolioFilterFormValue } from './interfaces'; imports: [ FormsModule, GfEntityLogoComponent, - GfSymbolPipe, MatFormFieldModule, MatSelectModule, ReactiveFormsModule diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html index 12867662bb..d786d724de 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html @@ -25,7 +25,7 @@ } {{ lookupItem.symbol | gfSymbol }} + >{{ lookupItem.symbol }} @if (lookupItem.currency) { · {{ lookupItem.currency }} } diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index 87d43d764c..cab911ef2b 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -1,5 +1,4 @@ import { LookupItem } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { DataService } from '@ghostfolio/ui/services'; import { FocusMonitor } from '@angular/cdk/a11y'; @@ -58,7 +57,6 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; imports: [ FormsModule, GfPremiumIndicatorComponent, - GfSymbolPipe, MatAutocompleteModule, MatFormFieldModule, MatInputModule, diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html index e07e5fe4d9..3e09ab5bab 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -80,7 +80,7 @@
{{ parentHolding?.name }}
{{ - parentHolding?.symbol | gfSymbol + parentHolding?.symbol }}
diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 51ef8751fe..3e7473c656 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -3,7 +3,6 @@ import { AssetProfileIdentifier, HoldingWithParents } from '@ghostfolio/common/interfaces'; -import { GfSymbolPipe } from '@ghostfolio/common/pipes'; import { animate, @@ -43,7 +42,6 @@ import { GfValueComponent } from '../value/value.component'; ], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - GfSymbolPipe, GfValueComponent, MatButtonModule, MatPaginatorModule, From faf01ae820fcd4c0948993e8034802a3209bf798 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:15:56 +0200 Subject: [PATCH 6/9] Feature/support migrating asset profile to MANUAL data source (#7376) * Support migrating asset profile to MANUAL data source * Update changelog --- CHANGELOG.md | 4 + apps/api/src/app/admin/admin.service.ts | 113 +++++++++++----- .../market-data/market-data.service.ts | 2 +- .../symbol-profile/symbol-profile.service.ts | 9 ++ .../admin-market-data.component.ts | 17 ++- .../asset-profile-dialog.component.ts | 124 +++++++++++------- .../asset-profile-dialog.html | 13 ++ 7 files changed, 193 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4a3bc176..2ca1c9291f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for converting an asset profile to the `MANUAL` data source in the asset profile details dialog of the admin control panel + ### Changed - Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 8325fa90ec..26a4e06f47 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -12,6 +12,7 @@ import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; import { + applyAssetProfileOverrides, getAssetProfileIdentifier, getCurrencyFromSymbol } from '@ghostfolio/common/helper'; @@ -39,6 +40,7 @@ import { } from '@prisma/client'; import { differenceInDays } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { randomUUID } from 'node:crypto'; @Injectable() export class AdminService { @@ -240,16 +242,25 @@ export class AdminService { url }: Prisma.SymbolProfileUpdateInput ) { + const isConversionToManualDataSource = + newDataSource === DataSource.MANUAL && dataSource !== DataSource.MANUAL; + + if (isConversionToManualDataSource && !newSymbol) { + newSymbol = randomUUID(); + } + if ( - newSymbol && newDataSource && - (newSymbol !== symbol || newDataSource !== dataSource) + newSymbol && + (newDataSource !== dataSource || newSymbol !== symbol) ) { + const newAssetProfileIdentifier: AssetProfileIdentifier = { + dataSource: newDataSource as DataSource, + symbol: newSymbol as string + }; + const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ - { - dataSource: DataSource[newDataSource.toString()], - symbol: newSymbol as string - } + newAssetProfileIdentifier ]); if (assetProfile) { @@ -259,45 +270,79 @@ export class AdminService { ); } - try { - await Promise.all([ - this.symbolProfileService.updateAssetProfileIdentifier( - { - dataSource, - symbol - }, - { - dataSource: DataSource[newDataSource.toString()], - symbol: newSymbol as string - } + const operations: Prisma.PrismaPromise[] = [ + this.symbolProfileService.updateAssetProfileIdentifier( + { + dataSource, + symbol + }, + newAssetProfileIdentifier + ), + this.marketDataService.updateAssetProfileIdentifier( + { + dataSource, + symbol + }, + newAssetProfileIdentifier + ) + ]; + + if (isConversionToManualDataSource) { + const currentAssetProfile = + await this.prismaService.symbolProfile.findUnique({ + include: { assetProfileOverrides: true }, + where: { dataSource_symbol: { dataSource, symbol } } + }); + + if (!currentAssetProfile) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + const currentAssetProfileWithOverrides = applyAssetProfileOverrides( + currentAssetProfile, + currentAssetProfile.assetProfileOverrides + ); + + operations.push( + // The overrides are applied on every read, so delete them and + // persist the merged values in the asset profile instead + this.symbolProfileService.deleteAssetProfileOverrides( + newAssetProfileIdentifier ), - this.marketDataService.updateAssetProfileIdentifier( + this.symbolProfileService.updateSymbolProfile( + newAssetProfileIdentifier, { - dataSource, - symbol - }, - { - dataSource: DataSource[newDataSource.toString()], - symbol: newSymbol as string + assetClass: currentAssetProfileWithOverrides.assetClass, + assetSubClass: currentAssetProfileWithOverrides.assetSubClass, + countries: + currentAssetProfileWithOverrides.countries ?? undefined, + holdings: currentAssetProfileWithOverrides.holdings ?? undefined, + name: currentAssetProfileWithOverrides.name, + sectors: currentAssetProfileWithOverrides.sectors ?? undefined, + url: currentAssetProfileWithOverrides.url } ) - ]); - - const [updatedAssetProfile] = - await this.symbolProfileService.getSymbolProfiles([ - { - dataSource: DataSource[newDataSource.toString()], - symbol: newSymbol as string - } - ]); + ); + } - return updatedAssetProfile; + try { + await this.prismaService.$transaction(operations); } catch { throw new HttpException( getReasonPhrase(StatusCodes.BAD_REQUEST), StatusCodes.BAD_REQUEST ); } + + const [updatedAssetProfile] = + await this.symbolProfileService.getSymbolProfiles([ + newAssetProfileIdentifier + ]); + + return updatedAssetProfile; } else { const assetProfileOverrides = { assetClass: assetClass as AssetClass, diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 086434724b..ad388ce5c5 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -204,7 +204,7 @@ export class MarketDataService { ); } - public async updateAssetProfileIdentifier( + public updateAssetProfileIdentifier( oldAssetProfileIdentifier: AssetProfileIdentifier, newAssetProfileIdentifier: AssetProfileIdentifier ) { diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 7157f0856d..3f40db247e 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -35,6 +35,15 @@ export class SymbolProfileService { }); } + public deleteAssetProfileOverrides({ + dataSource, + symbol + }: AssetProfileIdentifier) { + return this.prismaService.assetProfileOverrides.deleteMany({ + where: { symbolProfile: { dataSource, symbol } } + }); + } + public async deleteById(id: string) { return this.prismaService.symbolProfile.delete({ where: { id } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index e716907ef3..b29ccc7d7b 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -408,7 +408,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { const dialogRef = this.dialog.open< GfAssetProfileDialogComponent, - AssetProfileDialogParams + AssetProfileDialogParams, + AssetProfileIdentifier >(GfAssetProfileDialogComponent, { autoFocus: false, data: { @@ -426,15 +427,13 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { dialogRef .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe( - (newAssetProfileIdentifier: AssetProfileIdentifier | undefined) => { - if (newAssetProfileIdentifier) { - this.onOpenAssetProfileDialog(newAssetProfileIdentifier); - } else { - this.router.navigate(['.'], { relativeTo: this.route }); - } + .subscribe((newAssetProfileIdentifier) => { + if (newAssetProfileIdentifier) { + this.onOpenAssetProfileDialog(newAssetProfileIdentifier); + } else { + this.router.navigate(['.'], { relativeTo: this.route }); } - ); + }); }); } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index c34e8eb78c..c5562740fd 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -5,6 +5,7 @@ import { PROPERTY_IS_DATA_GATHERING_ENABLED } from '@ghostfolio/common/config'; import { UpdateAssetProfileDto } from '@ghostfolio/common/dtos'; +import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { canDeleteAssetProfile, DATE_FORMAT, @@ -75,6 +76,7 @@ import { AssetClass, AssetSubClass, DataGatheringFrequency, + DataSource, MarketData, Prisma, SymbolProfile @@ -215,6 +217,8 @@ export class GfAssetProfileDialogComponent implements OnInit { } ]; + protected readonly DataSource = DataSource; + protected readonly dateRangeOptions = [ { label: $localize`Current week` + ' (' + $localize`WTD` + ')', @@ -277,7 +281,10 @@ export class GfAssetProfileDialogComponent implements OnInit { @Inject(MAT_DIALOG_DATA) protected data: AssetProfileDialogParams, private dataService: DataService, private destroyRef: DestroyRef, - private dialogRef: MatDialogRef, + private dialogRef: MatDialogRef< + GfAssetProfileDialogComponent, + AssetProfileIdentifier + >, private formBuilder: FormBuilder, private notificationService: NotificationService, private snackBar: MatSnackBar, @@ -467,6 +474,19 @@ export class GfAssetProfileDialogComponent implements OnInit { this.dialogRef.close(); } + protected onConvertToManualDataSource() { + this.patchAssetProfileIdentifier({ + getErrorMessage: () => { + return ( + '😞 ' + + $localize`An error occurred while converting the data source to ${DataSource.MANUAL}.` + ); + }, + title: $localize`Do you really want to convert the data source to ${DataSource.MANUAL}?`, + updateAssetProfileDto: { dataSource: DataSource.MANUAL } + }); + } + protected onDeleteProfileData({ dataSource, symbol @@ -666,13 +686,16 @@ export class GfAssetProfileDialogComponent implements OnInit { } protected async onSubmitAssetProfileIdentifierForm() { + const newAssetProfileIdentifier = + this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value; + + if (!newAssetProfileIdentifier?.dataSource) { + return; + } + const assetProfileIdentifier: UpdateAssetProfileDto = { - dataSource: - this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value - ?.dataSource ?? undefined, - symbol: - this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value - ?.symbol ?? undefined + dataSource: newAssetProfileIdentifier.dataSource, + symbol: newAssetProfileIdentifier.symbol }; try { @@ -687,46 +710,19 @@ export class GfAssetProfileDialogComponent implements OnInit { return; } - this.adminService - .patchAssetProfile( - { - dataSource: this.data.dataSource, - symbol: this.data.symbol - }, - assetProfileIdentifier - ) - .pipe( - catchError((error: HttpErrorResponse) => { - if (error.status === StatusCodes.CONFLICT) { - this.snackBar.open( - $localize`${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}) is already in use.`, - undefined, - { - duration: ms('3 seconds') - } - ); - } else { - this.snackBar.open( - $localize`An error occurred while updating to ${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}).`, - undefined, - { - duration: ms('3 seconds') - } - ); - } + this.patchAssetProfileIdentifier({ + getErrorMessage: (error) => { + if (error.status === StatusCodes.CONFLICT) { + // TODO: Ask if the user wants to merge the two asset profiles - return EMPTY; - }), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe(() => { - const newAssetProfileIdentifier = { - dataSource: assetProfileIdentifier.dataSource, - symbol: assetProfileIdentifier.symbol - }; + return $localize`${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}) is already in use.`; + } - this.dialogRef.close(newAssetProfileIdentifier); - }); + return $localize`An error occurred while updating to ${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}).`; + }, + title: $localize`Do you really want to convert this asset profile to ${newAssetProfileIdentifier.symbol} (${newAssetProfileIdentifier.dataSource})?`, + updateAssetProfileDto: assetProfileIdentifier + }); } protected onTestMarketData() { @@ -824,4 +820,42 @@ export class GfAssetProfileDialogComponent implements OnInit { return null; } + + private patchAssetProfileIdentifier({ + getErrorMessage, + title, + updateAssetProfileDto + }: { + getErrorMessage: (error: HttpErrorResponse) => string; + title: string; + updateAssetProfileDto: UpdateAssetProfileDto; + }) { + this.notificationService.confirm({ + title, + confirmFn: () => { + this.adminService + .patchAssetProfile( + { + dataSource: this.data.dataSource, + symbol: this.data.symbol + }, + updateAssetProfileDto + ) + .pipe( + catchError((error: HttpErrorResponse) => { + this.snackBar.open(getErrorMessage(error), undefined, { + duration: ms('3 seconds') + }); + + return EMPTY; + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(({ dataSource, symbol }) => { + this.dialogRef.close({ dataSource, symbol }); + }); + }, + confirmType: ConfirmationDialogType.Primary + }); + } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index 9b69ef6fce..c9abdeeb7c 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -162,6 +162,19 @@ Cancel + @if (data.dataSource !== DataSource.MANUAL) { +

or

+ + }
} @else {
From 3739b768856fc4148027af44b3c22c6592bc268c Mon Sep 17 00:00:00 2001 From: Arham Amin <132888838+arhxam@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:12:32 +0530 Subject: [PATCH 7/9] Task/preserve negative numbers in extractNumberFromString (#7377) * Preserve negative numbers in extractNumberFromString * Update changelog --- CHANGELOG.md | 1 + libs/common/src/lib/helper.spec.ts | 20 ++++++++++++++++++++ libs/common/src/lib/helper.ts | 7 ++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca1c9291f..82954caa4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extended the `extractNumberFromString()` function to support negative values - Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users - Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`) - Simplified the `getHistorical()` function response in the data provider interface diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index 6a6fe47734..d33f104522 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -11,6 +11,10 @@ describe('Helper', () => { expect(extractNumberFromString({ value: '999.99' })).toEqual(999.99); }); + it('Get negative decimal number', () => { + expect(extractNumberFromString({ value: '-999.99' })).toEqual(-999.99); + }); + it('Get decimal number (with spaces)', () => { expect(extractNumberFromString({ value: ' 999.99 ' })).toEqual(999.99); }); @@ -19,6 +23,12 @@ describe('Helper', () => { expect(extractNumberFromString({ value: '999.99 CHF' })).toEqual(999.99); }); + it('Get negative decimal number (with currency)', () => { + expect(extractNumberFromString({ value: '-999.99 CHF' })).toEqual( + -999.99 + ); + }); + it('Get decimal number (comma notation)', () => { expect( extractNumberFromString({ locale: 'de-DE', value: '999,99' }) @@ -37,12 +47,22 @@ describe('Helper', () => { ).toEqual(99999.99); }); + it('Get negative decimal number with group (comma notation)', () => { + expect( + extractNumberFromString({ locale: 'de-DE', value: '-99.999,99' }) + ).toEqual(-99999.99); + }); + it('Get decimal number (comma notation) for locale where currency is not grouped by default', () => { expect( extractNumberFromString({ locale: 'es-ES', value: '999,99' }) ).toEqual(999.99); }); + it('Get decimal number (with hyphenated text)', () => { + expect(extractNumberFromString({ value: 'BRK-B 425.30' })).toEqual(425.3); + }); + it('Not a number', () => { expect(extractNumberFromString({ value: 'X' })).toEqual(NaN); }); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 6a135eef63..ad8674d815 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -209,12 +209,17 @@ export function extractNumberFromString({ value: string; }): number | undefined { try { + // Only a leading minus sign indicates a negative value. Detect it before + // stripping so that hyphens within the text cannot flip the sign. + const isNegative = value.trim().startsWith('-'); + // Remove non-numeric characters (excluding international formatting characters) const numericValue = value.replace(/[^\d.,'’\s]/g, ''); const parser = new NumberParser(locale); + const parsedValue = parser.parse(numericValue); - return parser.parse(numericValue); + return isNegative ? -parsedValue : parsedValue; } catch { return undefined; } From 849a6671eca953592db8ff7950fcda741a9d4b10 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:45:41 +0200 Subject: [PATCH 8/9] Task/upgrade bull-board to version 8.1.2 (#7381) * Update bull-board to version 8.1.2 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 6 +++--- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82954caa4d..f33b7ab5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users - Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`) - Simplified the `getHistorical()` function response in the data provider interface +- Upgraded `bull-board` from version `8.0.1` to `8.1.2` ## 3.29.0 - 2026-07-18 diff --git a/package-lock.json b/package-lock.json index ccd0388142..98547d186a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "8.0.1", - "@bull-board/express": "8.0.1", - "@bull-board/nestjs": "8.0.1", + "@bull-board/api": "8.1.2", + "@bull-board/express": "8.1.2", + "@bull-board/nestjs": "8.1.2", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.7", @@ -3548,25 +3548,25 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bull-board/api": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-8.0.1.tgz", - "integrity": "sha512-7FELJHRQPtjH9+r/DUArr4pDVU8r1yeDS9azQUzFtIbsUT5xGjyg5R1RB0I/RfwFfK5bqho/4cpzxJ/UnQjSKA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-8.1.2.tgz", + "integrity": "sha512-6NGYCIRhHJmmoCwAFA1z4lPpR/D/BdOjyERd2MMeG4Samu05FSjzKKdN55S3ga30EcXeENfYzmNn6IDmVz5OTg==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "8.0.1" + "@bull-board/ui": "8.1.2" } }, "node_modules/@bull-board/express": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-8.0.1.tgz", - "integrity": "sha512-VOEhLNlaaVk3mBBoREXO/Dopzr9rKK5TpOGyaCbcnpzLKlrlwhj6BAeWWreB7/3Wu+pNnbKmrAhK8OOnYdzxLg==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-8.1.2.tgz", + "integrity": "sha512-IldpQLXlezJRzk1BVNJjH+Oi3NBRcBopq9bI2ndLs0R+ySK1ux+TtGDUW+eX2fl0wEHwSpuUgPBAJID695zohA==", "license": "MIT", "dependencies": { - "@bull-board/api": "8.0.1", - "@bull-board/ui": "8.0.1", + "@bull-board/api": "8.1.2", + "@bull-board/ui": "8.1.2", "ejs": "^6.0.1", "express": "^5.2.1" } @@ -3584,12 +3584,12 @@ } }, "node_modules/@bull-board/nestjs": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-8.0.1.tgz", - "integrity": "sha512-yBB+S7ibdrcO6y01VQTzd51Qxx19bPnte5TSdA59BWSeH6oVagB+EkCqHXTbfdGrvLrwZEF3tnSCE6339nB7MQ==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-8.1.2.tgz", + "integrity": "sha512-7sgESSitxULFSxuWIYx6zUyCgQIVFtw+s5ezGFvdh3/s2dwP2NoOQ76xJn+16cXh0EAFM+rN1f05BAtc0aLlUQ==", "license": "MIT", "peerDependencies": { - "@bull-board/api": "^8.0.1", + "@bull-board/api": "^8.1.2", "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", @@ -3598,12 +3598,12 @@ } }, "node_modules/@bull-board/ui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-8.0.1.tgz", - "integrity": "sha512-gKEGSD8dlUoWFGJJ4I4Q5bDtfB7yJwGfpgA2DVn1G1TzlUSN5n4fzzcGpLf5fS+QhR7tB/niydUiFRQ2zrjVrQ==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-8.1.2.tgz", + "integrity": "sha512-gC5u9XUSiRile4/VC+WHXtaPvfZV4Mrrz8MSvPe32Hjl2MXStfSuEOb+9Tw/WMpNSZ442tGpFZLz0RDh1Yo3jg==", "license": "MIT", "dependencies": { - "@bull-board/api": "8.0.1" + "@bull-board/api": "8.1.2" } }, "node_modules/@cacheable/utils": { diff --git a/package.json b/package.json index 14d4f5f112..4355877fd7 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "8.0.1", - "@bull-board/express": "8.0.1", - "@bull-board/nestjs": "8.0.1", + "@bull-board/api": "8.1.2", + "@bull-board/express": "8.1.2", + "@bull-board/nestjs": "8.1.2", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.7", From d02db5002e7d139c045daef2252c3d89d312499b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:47:46 +0200 Subject: [PATCH 9/9] Release 3.30.0 (#7382) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f33b7ab5e3..f83aa49b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.30.0 - 2026-07-19 ### Added diff --git a/package-lock.json b/package-lock.json index 98547d186a..946b8a0a58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.29.0", + "version": "3.30.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.29.0", + "version": "3.30.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 4355877fd7..0376d9877b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.29.0", + "version": "3.30.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",