From 61e9a761a9f9194bcff896f67ce5eebe9977ca9c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:21:26 +0200 Subject: [PATCH 1/6] Task/set change detection strategy to OnPush in various page components (#7289) * Set change detection strategy to OnPush * Update changelog --- CHANGELOG.md | 7 +++++++ .../src/app/pages/about/about-page.component.ts | 12 +++++++++--- .../src/app/pages/admin/admin-page.component.ts | 11 ++++++++++- apps/client/src/app/pages/faq/faq-page.component.ts | 3 ++- .../client/src/app/pages/home/home-page.component.ts | 6 ++++-- .../app/pages/resources/resources-page.component.ts | 3 ++- .../user-account/user-account-page.component.ts | 12 +++++++++--- apps/client/src/app/pages/zen/zen-page.component.ts | 12 +++++++++--- 8 files changed, 52 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0d44848..d96f7dccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Migrated the deprecated `@nx/webpack:webpack` executor to `@nx/webpack/plugin` +- Set the change detection strategy to `OnPush` in the about page +- Set the change detection strategy to `OnPush` in the admin control panel - Set the change detection strategy to `OnPush` in the blog page components +- Set the change detection strategy to `OnPush` in the Frequently Asked Questions (FAQ) page +- Set the change detection strategy to `OnPush` in the home page - Set the change detection strategy to `OnPush` in the markets overview +- Set the change detection strategy to `OnPush` in the resources page +- Set the change detection strategy to `OnPush` in the user account page +- Set the change detection strategy to `OnPush` in the _Zen Mode_ - Improved the language localization for Chinese (`zh`) ## 3.22.0 - 2026-07-08 diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 616977d80..615c0897d 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -8,7 +8,12 @@ import { } from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { @@ -21,6 +26,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-about-page', @@ -83,8 +89,6 @@ export class AboutPageComponent { }); this.user = state.user; - - this.changeDetectorRef.markForCheck(); } this.tabs.push({ @@ -92,6 +96,8 @@ export class AboutPageComponent { label: publicRoutes.about.subRoutes.ossFriends.title, routerLink: publicRoutes.about.subRoutes.ossFriends.routerLink }); + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index eb4ddb95c..15957d346 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -12,7 +12,12 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { Component, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { @@ -24,6 +29,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-admin-page', @@ -35,6 +41,7 @@ export class AdminPageComponent { private user: User; + private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly tokenStorageService = inject(TokenStorageService); private readonly userService = inject(UserService); @@ -45,6 +52,8 @@ export class AdminPageComponent { this.user = state?.user; this.initializeTabs(); + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/faq/faq-page.component.ts b/apps/client/src/app/pages/faq/faq-page.component.ts index 60081687a..17f70d138 100644 --- a/apps/client/src/app/pages/faq/faq-page.component.ts +++ b/apps/client/src/app/pages/faq/faq-page.component.ts @@ -6,11 +6,12 @@ import { } from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-faq-page', diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 8c5caab22..574286c82 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -9,6 +9,7 @@ import { } from '@ghostfolio/ui/page-tabs'; import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, @@ -25,6 +26,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-home-page', @@ -85,9 +87,9 @@ export class GfHomePageComponent implements OnInit { : internalRoutes.home.subRoutes.markets.routerLink } ]; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/resources/resources-page.component.ts b/apps/client/src/app/pages/resources/resources-page.component.ts index 52980be85..d88aed4ba 100644 --- a/apps/client/src/app/pages/resources/resources-page.component.ts +++ b/apps/client/src/app/pages/resources/resources-page.component.ts @@ -4,7 +4,7 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { bookOutline, @@ -14,6 +14,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-resources-page', diff --git a/apps/client/src/app/pages/user-account/user-account-page.component.ts b/apps/client/src/app/pages/user-account/user-account-page.component.ts index ec7547b3c..486506675 100644 --- a/apps/client/src/app/pages/user-account/user-account-page.component.ts +++ b/apps/client/src/app/pages/user-account/user-account-page.component.ts @@ -6,12 +6,18 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-user-account-page', @@ -52,9 +58,9 @@ export class GfUserAccountPageComponent { routerLink: internalRoutes.account.subRoutes.access.routerLink } ]; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ diamondOutline, keyOutline, settingsOutline }); diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index 4a897093c..0eb55ae26 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -6,12 +6,18 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { albumsOutline, analyticsOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-zen-page', @@ -44,9 +50,9 @@ export class GfZenPageComponent { } ]; this.user = state.user; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ albumsOutline, analyticsOutline }); From d10e815a3dd5c68e11415f4972fc4bed8efd2658 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:52:16 +0700 Subject: [PATCH 2/6] Task/improve type safety in portfolio summary component (#7293) * fix(client): resolve type errors * feat(client): enforce encapsulation * feat(client): replace constructor based DI with inject functions * feat(client): implement output signal --- .../portfolio-summary.component.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts index 3d2760202..b1e32c6c3 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -8,10 +8,10 @@ import { GfValueComponent } from '@ghostfolio/ui/value'; import { ChangeDetectionStrategy, Component, - EventEmitter, + inject, Input, OnChanges, - Output + output } from '@angular/core'; import { MatTooltipModule } from '@angular/material/tooltip'; import { IonIcon } from '@ionic/angular/standalone'; @@ -40,44 +40,46 @@ export class GfPortfolioSummaryComponent implements OnChanges { @Input() summary: PortfolioSummary; @Input() user: User; - @Output() emergencyFundChanged = new EventEmitter(); + public emergencyFundChanged = output(); - public buyAndSellActivitiesTooltip = translate( + protected readonly buyAndSellActivitiesTooltip = translate( 'BUY_AND_SELL_ACTIVITIES_TOOLTIP' ); - public precision = 2; - public timeInMarket: string; + protected precision = 2; + protected timeInMarket: string | undefined; - public get buyingPowerPercentage() { + private readonly notificationService = inject(NotificationService); + + public constructor() { + addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline }); + } + + protected get buyingPowerPercentage() { return this.summary?.totalValueInBaseCurrency ? this.summary.cash / this.summary.totalValueInBaseCurrency : 0; } - public get emergencyFundPercentage() { + protected get emergencyFundPercentage() { return this.summary?.totalValueInBaseCurrency ? (this.summary.emergencyFund?.total || 0) / this.summary.totalValueInBaseCurrency : 0; } - public get excludedFromAnalysisPercentage() { + protected get excludedFromAnalysisPercentage() { return this.summary?.totalValueInBaseCurrency ? this.summary.excludedAccountsAndActivities / this.summary.totalValueInBaseCurrency : 0; } - public constructor(private notificationService: NotificationService) { - addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline }); - } - public ngOnChanges() { if (this.summary) { if ( this.deviceType === 'mobile' && - this.summary.totalValueInBaseCurrency >= + (this.summary.totalValueInBaseCurrency ?? 0) >= NUMERICAL_PRECISION_THRESHOLD_6_FIGURES ) { this.precision = 0; @@ -98,7 +100,7 @@ export class GfPortfolioSummaryComponent implements OnChanges { } } - public onEditEmergencyFund() { + protected onEditEmergencyFund() { this.notificationService.prompt({ confirmFn: (value) => { const emergencyFund = parseFloat(value.trim()) || 0; From 4e6a1ede7be2d5e37de987f6ed0652954fe56569 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:52:43 +0200 Subject: [PATCH 3/6] Task/improve language localization for DE (20260709) (#7291) * Update translation * Update changelog --- CHANGELOG.md | 1 + apps/client/src/locales/messages.de.xlf | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96f7dccb..b0eb7bd50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Set the change detection strategy to `OnPush` in the user account page - Set the change detection strategy to `OnPush` in the _Zen Mode_ - Improved the language localization for Chinese (`zh`) +- Improved the language localization for German (`de`) ## 3.22.0 - 2026-07-08 diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 1ae8a60b8..eab9ec2cb 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -1739,7 +1739,7 @@ popular - popular + beliebt apps/client/src/app/components/admin-settings/admin-settings.component.html 79 From 2087ade01338578be6c5cb68ae5819ee3aec9359 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:38:19 +0200 Subject: [PATCH 4/6] Release 3.23.0 (#7299) --- 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 b0eb7bd50..51d049e9a 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.23.0 - 2026-07-10 ### Changed diff --git a/package-lock.json b/package-lock.json index 30467139c..d8c4974d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.23.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 2de43ba7a..261d35b54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.23.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From ac8616856a152003c8ba4e8fd952e82096a17b63 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:33:16 +0200 Subject: [PATCH 5/6] Task/expose data source for Fear & Greed Index (#7285) * Expose data source for Fear & Greed Index * Update changelog --- CHANGELOG.md | 6 ++++ .../market-data/market-data.controller.ts | 6 ++-- .../market-data/market-data.module.ts | 8 ++++- apps/api/src/app/info/info.service.ts | 13 +++++--- .../configuration/configuration.service.ts | 3 ++ .../data-provider/data-provider.service.ts | 6 ++++ .../rapid-api/rapid-api.service.ts | 32 +++++++++++-------- .../interfaces/environment.interface.ts | 1 + .../twitter-bot/twitter-bot.module.ts | 8 ++++- .../twitter-bot/twitter-bot.service.ts | 10 +++--- libs/common/src/lib/config.ts | 1 - 11 files changed, 65 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d049e9a..c139319a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 + +### Added + +- Exposed the `DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS` environment variable to set the data source of the _Fear & Greed Index_ (market mood) + ## 3.23.0 - 2026-07-10 ### Changed diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts index 5dad0511f..105bb0780 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts @@ -1,11 +1,11 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, - ghostfolioFearAndGreedIndexDataSourceStocks, ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config'; @@ -36,6 +36,7 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes'; @Controller('market-data') export class MarketDataController { public constructor( + private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly symbolProfileService: SymbolProfileService, @@ -64,7 +65,8 @@ export class MarketDataController { this.symbolService.get({ includeHistoricalData, dataGatheringItem: { - dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), symbol: ghostfolioFearAndGreedIndexSymbolStocks }, useIntradayData: true diff --git a/apps/api/src/app/endpoints/market-data/market-data.module.ts b/apps/api/src/app/endpoints/market-data/market-data.module.ts index 1de10907b..47ce11502 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.module.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.module.ts @@ -1,4 +1,5 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -8,6 +9,11 @@ import { MarketDataController } from './market-data.controller'; @Module({ controllers: [MarketDataController], - imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule] + imports: [ + DataProviderModule, + MarketDataServiceModule, + SymbolModule, + SymbolProfileModule + ] }) export class MarketDataModule {} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 4441036a2..10836f7b8 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -4,6 +4,7 @@ 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 { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { @@ -15,8 +16,7 @@ import { PROPERTY_GITHUB_STARGAZERS, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, - PROPERTY_UPTIME, - ghostfolioFearAndGreedIndexDataSourceStocks + PROPERTY_UPTIME } from '@ghostfolio/common/config'; import { InfoItem, Statistics } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -33,6 +33,7 @@ export class InfoService { public constructor( private readonly benchmarkService: BenchmarkService, private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly jwtService: JwtService, private readonly propertyService: PropertyService, @@ -60,13 +61,15 @@ 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( - ghostfolioFearAndGreedIndexDataSourceStocks + fearAndGreedIndexDataSource ); } else { - info.fearAndGreedDataSource = - ghostfolioFearAndGreedIndexDataSourceStocks; + info.fearAndGreedDataSource = fearAndGreedIndexDataSource; } globalPermissions.push(permissions.enableFearAndGreedIndex); diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index c96ccd946..b097627df 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -34,6 +34,9 @@ export class ConfigurationService { CACHE_QUOTES_TTL: num({ default: ms('1 minute') }), CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), + DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: str({ + default: DataSource.RAPID_API + }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCES: json({ default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index c6e9c83c1..8723466b0 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -177,6 +177,12 @@ export class DataProviderService implements OnModuleInit { ]; } + public getDataSourceForFearAndGreedIndexStocks(): DataSource { + return DataSource[ + this.configurationService.get('DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS') + ]; + } + public getDataSourceForImport(): DataSource { return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')]; } diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index d8ba1ec67..e704f2861 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -64,13 +64,15 @@ export class RapidApiService implements DataProviderInterface { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { const fgi = await this.getFearAndGreedIndex(); - return { - [symbol]: { - [format(getYesterday(), DATE_FORMAT)]: { - marketPrice: fgi.previousClose.value + if (fgi) { + return { + [symbol]: { + [format(getYesterday(), DATE_FORMAT)]: { + marketPrice: fgi.previousClose.value + } } - } - }; + }; + } } } catch (error) { throw new Error( @@ -101,14 +103,16 @@ export class RapidApiService implements DataProviderInterface { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { const fgi = await this.getFearAndGreedIndex(); - return { - [symbol]: { - currency: undefined, - dataSource: this.getName(), - marketPrice: fgi.now.value, - marketState: 'open' - } - }; + if (fgi) { + return { + [symbol]: { + currency: undefined, + dataSource: this.getName(), + marketPrice: fgi.now.value, + marketState: 'open' + } + }; + } } } catch (error) { this.logger.error(error); diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 89463e201..6147d4e45 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -13,6 +13,7 @@ export interface Environment extends CleanedEnvAccessors { CACHE_QUOTES_TTL: number; CACHE_TTL: number; DATA_SOURCE_EXCHANGE_RATES: string; + DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: string; DATA_SOURCE_IMPORT: string; DATA_SOURCES: string[]; DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts index 80d53169c..bdb1a7988 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.module.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts @@ -1,13 +1,19 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; import { Module } from '@nestjs/common'; @Module({ exports: [TwitterBotService], - imports: [BenchmarkModule, ConfigurationModule, SymbolModule], + imports: [ + BenchmarkModule, + ConfigurationModule, + DataProviderModule, + SymbolModule + ], providers: [TwitterBotService] }) export class TwitterBotModule {} diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index 366b016b6..8dffddf7b 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -1,10 +1,8 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { - ghostfolioFearAndGreedIndexDataSourceStocks, - ghostfolioFearAndGreedIndexSymbolStocks -} from '@ghostfolio/common/config'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config'; import { resolveFearAndGreedIndex, resolveMarketCondition @@ -24,6 +22,7 @@ export class TwitterBotService implements OnModuleInit { public constructor( private readonly benchmarkService: BenchmarkService, private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, private readonly symbolService: SymbolService ) {} @@ -49,7 +48,8 @@ export class TwitterBotService implements OnModuleInit { try { const symbolItem = await this.symbolService.get({ dataGatheringItem: { - dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), symbol: ghostfolioFearAndGreedIndexSymbolStocks } }); diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index c60170bbb..d613fb3cf 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -11,7 +11,6 @@ export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = DataSource.MANUAL; -export const ghostfolioFearAndGreedIndexDataSourceStocks = DataSource.RAPID_API; export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`; export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`; From cb692f8d06f710274a9bfcb57a856d6b3fbbc320 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:33:55 +0700 Subject: [PATCH 6/6] Task/improve type safety in user detail dialog component (#7292) Improve type safety --- .../user-detail-dialog.component.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index 73e09f612..637fe8c31 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -9,7 +9,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, - Inject, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -49,28 +49,30 @@ import { templateUrl: './user-detail-dialog.html' }) export class GfUserDetailDialogComponent implements OnInit { - public baseCurrency: string; - public readonly getCountryName = getCountryName; - public subscriptionsDataSource = new MatTableDataSource(); - public subscriptionsDisplayedColumns = [ + protected baseCurrency: string; + protected readonly getCountryName = getCountryName; + protected readonly subscriptionsDataSource = + new MatTableDataSource(); + protected readonly subscriptionsDisplayedColumns = [ 'createdAt', 'type', 'price', 'expiresAt' ]; - public user: AdminUserResponse; - - public constructor( - private adminService: AdminService, - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, - private dataService: DataService, - private destroyRef: DestroyRef, - public dialogRef: MatDialogRef< - GfUserDetailDialogComponent, - UserDetailDialogResult - > - ) { + protected user: AdminUserResponse; + + protected readonly data = inject(MAT_DIALOG_DATA); + + private readonly adminService = inject(AdminService); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly dialogRef = + inject>( + MatDialogRef + ); + + public constructor() { this.baseCurrency = this.dataService.fetchInfo().baseCurrency; addIcons({ @@ -98,14 +100,14 @@ export class GfUserDetailDialogComponent implements OnInit { }); } - public deleteUser() { + protected deleteUser() { this.dialogRef.close({ action: 'delete', userId: this.data.userId }); } - public getSum() { + protected getSum() { return getSum( this.subscriptionsDataSource.data .filter(({ price }) => { @@ -117,7 +119,7 @@ export class GfUserDetailDialogComponent implements OnInit { ).toNumber(); } - public getType({ createdAt, expiresAt, price }: Subscription) { + protected getType({ createdAt, expiresAt, price }: Subscription) { if (price) { return $localize`Paid`; } @@ -127,7 +129,7 @@ export class GfUserDetailDialogComponent implements OnInit { : $localize`Coupon`; } - public onClose() { + protected onClose() { this.dialogRef.close(); } }