From dbe0904aa0cb41229a4ef165c20b1eb1643ea5e4 Mon Sep 17 00:00:00 2001 From: David Requeno <108202767+DavidReque@users.noreply.github.com> Date: Sat, 18 Jul 2026 01:06:08 -0600 Subject: [PATCH 01/18] Task/add Storybook story for copy-to-clipboard functionality in value component (#7365) * Add Storybook story for copy-to-clipboard functionality * Update changelog --- CHANGELOG.md | 4 ++++ libs/ui/src/lib/value/value.component.stories.ts | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28e8145e9..58c5a68235 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 a _Storybook_ story for the copy-to-clipboard functionality in the value component + ### Changed - Improved the language localization for German (`de`) diff --git a/libs/ui/src/lib/value/value.component.stories.ts b/libs/ui/src/lib/value/value.component.stories.ts index 214331efc9..5a285e89ba 100644 --- a/libs/ui/src/lib/value/value.component.stories.ts +++ b/libs/ui/src/lib/value/value.component.stories.ts @@ -1,5 +1,6 @@ import '@angular/localize/init'; -import { moduleMetadata } from '@storybook/angular'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { applicationConfig, moduleMetadata } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -9,6 +10,9 @@ export default { title: 'Value', component: GfValueComponent, decorators: [ + applicationConfig({ + providers: [provideNoopAnimations()] + }), moduleMetadata({ imports: [NgxSkeletonLoaderModule] }) @@ -103,3 +107,12 @@ export const Precision: Story = { value: 7.2534802394809285309 } }; + +export const WithCopyButton: Story = { + args: { + enableCopyToClipboardButton: true, + locale: 'en-US', + value: 1234.56 + }, + name: 'With Copy Button' +}; From 74e8df4ac9c07c6b67784d5b2342a9f5de5bec90 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:06:37 +0200 Subject: [PATCH 02/18] Task/upgrade stripe to version 22.3.2 (#7363) * Update stripe to version 22.3.2 * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/subscription/subscription.service.ts | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c5a68235..a485db50ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the language localization for German (`de`) +- Upgraded `stripe` from version `22.2.3` to `22.3.2` ## 3.28.0 - 2026-07-17 diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 07c2e7dbb8..83aee7c8e7 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -38,7 +38,7 @@ export class SubscriptionService { this.stripe = new Stripe( this.configurationService.get('STRIPE_SECRET_KEY'), { - apiVersion: '2026-05-27.dahlia' + apiVersion: '2026-06-24.dahlia' } ); } diff --git a/package-lock.json b/package-lock.json index 85a19972dc..adb5f9f9d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,7 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "22.2.3", + "stripe": "22.3.2", "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", @@ -32428,9 +32428,9 @@ } }, "node_modules/stripe": { - "version": "22.2.3", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.2.3.tgz", - "integrity": "sha512-9lrggvLtjO4Ef5WXH4t50ckHJcJgTD72xFB+KJgZCSszAH9zMV1BqU6PwmWbRLdcdX7LK6PbErBapiGK7pIb+g==", + "version": "22.3.2", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.3.2.tgz", + "integrity": "sha512-O13QOvgEIQvDlTy6Ubb5kB980wpbhmoZNsgCXKILjCMZS67f+bW+6w99k3gnSi/N1lkryoj1WYdpGT5Wc5edjg==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 6caa1604cf..521d73d996 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "22.2.3", + "stripe": "22.3.2", "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", From ae257f8acb2ea306eb21511879348236596dd5da Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:26:38 +0200 Subject: [PATCH 03/18] Feature/support Fear & Greed index via Ghostfolio data provider in self-hosted environments (#7364) * Support Fear & Greed index via Ghostfolio data provider in self-hosted environments * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/symbol/symbol.service.ts | 6 ++ apps/api/src/app/user/user.service.ts | 7 +++ .../data-provider/data-provider.service.ts | 31 ++++++++-- .../ghostfolio/ghostfolio.service.ts | 62 ++++++++++++++++++- .../interfaces/data-provider.interface.ts | 13 +++- 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a485db50ba..045fc395d7 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 ### Added +- Added support for the _Fear & Greed Index_ (market mood) via the `GHOSTFOLIO` data provider in self-hosted environments - Added a _Storybook_ story for the copy-to-clipboard functionality in the value component ### Changed diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 2ace4feeb2..98869797ea 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -133,6 +133,12 @@ export class SymbolService { }: { includeHistoricalData: number; }): Promise { + if (await this.dataProviderService.isDataProviderGhostfolioConfigured()) { + return this.dataProviderService.getMarketDataOfMarkets({ + includeHistoricalData + }); + } + const [ marketDataFearAndGreedIndexCryptocurrencies, marketDataFearAndGreedIndexStocks diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 4dcf160345..6304c81ea1 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -29,6 +29,7 @@ import { DEFAULT_DATE_RANGE, DEFAULT_LANGUAGE_CODE, DEFAULT_LOCALE, + PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_REFERRAL_PARTNERS, PROPERTY_SYSTEM_MESSAGE, @@ -546,6 +547,12 @@ export class UserService { if (hasRole(user, Role.ADMIN)) { currentPermissions.push(permissions.syncDemoUserAccount); } + } else { + if ( + await this.propertyService.getByKey(PROPERTY_API_KEY_GHOSTFOLIO) + ) { + currentPermissions.push(permissions.readMarketDataOfMarkets); + } } if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { 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 09bf1108eb..35dc97d2fd 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -27,7 +27,8 @@ import { DataProviderHistoricalResponse, DataProviderResponse, LookupItem, - LookupResponse + LookupResponse, + MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces'; import type { Granularity, UserWithSettings } from '@ghostfolio/common/types'; @@ -194,11 +195,7 @@ export class DataProviderService implements OnModuleInit { return DataSource[dataSource]; }); - const ghostfolioApiKey = await this.propertyService.getByKey( - PROPERTY_API_KEY_GHOSTFOLIO - ); - - if (ghostfolioApiKey) { + if (await this.isDataProviderGhostfolioConfigured()) { dataSources.push('GHOSTFOLIO'); } @@ -551,6 +548,22 @@ export class DataProviderService implements OnModuleInit { return result; } + public async getMarketDataOfMarkets({ + includeHistoricalData + }: { + includeHistoricalData: number; + }): Promise { + const dataProvider = this.getDataProvider(DataSource.GHOSTFOLIO); + + if (!dataProvider.getMarketDataOfMarkets) { + throw new Error( + `The data provider (${DataSource.GHOSTFOLIO}) does not support the market data of markets` + ); + } + + return dataProvider.getMarketDataOfMarkets({ includeHistoricalData }); + } + public async getQuotes({ items, requestTimeout, @@ -807,6 +820,12 @@ export class DataProviderService implements OnModuleInit { return response; } + public async isDataProviderGhostfolioConfigured(): Promise { + return !!(await this.propertyService.getByKey( + PROPERTY_API_KEY_GHOSTFOLIO + )); + } + public async search({ includeIndices = false, query, diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index 2b91855a6a..81b997037b 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -5,6 +5,7 @@ import { GetAssetProfileParams, GetDividendsParams, GetHistoricalParams, + GetMarketDataOfMarketsParams, GetQuotesParams, GetSearchParams } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; @@ -23,7 +24,9 @@ import { DividendsResponse, HistoricalResponse, LookupResponse, - QuotesResponse + MarketDataOfMarketsResponse, + QuotesResponse, + SymbolItem } from '@ghostfolio/common/interfaces'; import { Injectable, Logger } from '@nestjs/common'; @@ -224,6 +227,63 @@ export class GhostfolioService implements DataProviderInterface { } } + public async getMarketDataOfMarkets({ + includeHistoricalData = 0, + requestTimeout = this.configurationService.get('REQUEST_TIMEOUT') + }: GetMarketDataOfMarketsParams): Promise { + let marketDataOfMarkets: MarketDataOfMarketsResponse = { + fearAndGreedIndex: { + CRYPTOCURRENCIES: {} as SymbolItem, + STOCKS: {} as SymbolItem + } + }; + + try { + const queryParams = new URLSearchParams({ + includeHistoricalData: includeHistoricalData.toString() + }); + + const response = await this.fetchService.fetch( + `${this.URL}/v1/data-providers/ghostfolio/markets?${queryParams.toString()}`, + { + headers: await this.getRequestHeaders(), + signal: AbortSignal.timeout(requestTimeout) + } + ); + + if (!response.ok) { + throw new Response(await response.text(), { + status: response.status, + statusText: response.statusText + }); + } + + marketDataOfMarkets = + (await response.json()) as MarketDataOfMarketsResponse; + } catch (error) { + let message = error; + + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the market data of markets was aborted because the request to the data provider took more than ${( + requestTimeout / 1000 + ).toFixed(3)} seconds`; + } else if (error?.status === StatusCodes.TOO_MANY_REQUESTS) { + message = 'RequestError: The daily request limit has been exceeded'; + } else if ( + [StatusCodes.FORBIDDEN, StatusCodes.UNAUTHORIZED].includes( + error?.status + ) + ) { + message = + 'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.'; + } + + this.logger.error(message); + } + + return marketDataOfMarkets; + } + public getMaxNumberOfSymbolsPerRequest() { return 20; } diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index 5002fa87e1..8c2fb64d8b 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts @@ -2,7 +2,8 @@ import { DataProviderHistoricalResponse, DataProviderInfo, DataProviderResponse, - LookupResponse + LookupResponse, + MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces'; import { Granularity } from '@ghostfolio/common/types'; @@ -37,6 +38,11 @@ export interface DataProviderInterface { [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; }>; // TODO: Return only one symbol + getMarketDataOfMarkets?({ + includeHistoricalData, + requestTimeout + }: GetMarketDataOfMarketsParams): Promise; + getMaxNumberOfSymbolsPerRequest?(): number; getName(): DataSource; @@ -72,6 +78,11 @@ export interface GetHistoricalParams { to: Date; } +export interface GetMarketDataOfMarketsParams { + includeHistoricalData?: number; + requestTimeout?: number; +} + export interface GetQuotesParams { requestTimeout?: number; symbols: string[]; From 74914d973c8f37913550826bf7480f63e0929df6 Mon Sep 17 00:00:00 2001 From: Chanhyo Jung Date: Sat, 18 Jul 2026 16:56:01 +0900 Subject: [PATCH 04/18] Task/improve copy-to-clipboard feedback in value component (#7338) * Improve copy-to-clipboard feedback * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 1 + libs/ui/src/lib/value/value.component.html | 9 ++++-- libs/ui/src/lib/value/value.component.ts | 35 ++++++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045fc395d7..cc7ba56262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the copy-to-clipboard functionality in the value component by providing a visual confirmation - Improved the language localization for German (`de`) - Upgraded `stripe` from version `22.2.3` to `22.3.2` diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index d5476f42dd..14ac7154a2 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -13,13 +13,16 @@ @if (enableCopyToClipboardButton) { } diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index dffcb89e7e..494f7a347c 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -13,13 +13,14 @@ import { input, Input, OnChanges, + OnDestroy, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatSnackBar } from '@angular/material/snack-bar'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; -import { copyOutline } from 'ionicons/icons'; +import { checkmarkOutline, copyOutline } from 'ionicons/icons'; import { isNumber } from 'lodash'; import ms from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -32,7 +33,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; styleUrls: ['./value.component.scss'], templateUrl: './value.component.html' }) -export class GfValueComponent implements AfterViewInit, OnChanges { +export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() colorizeSign = false; @Input() deviceType: string; @Input() enableCopyToClipboardButton = false; @@ -54,22 +55,29 @@ export class GfValueComponent implements AfterViewInit, OnChanges { public absoluteValue = 0; public formattedValue = ''; public hasLabel = false; + public isCopied = false; public isNumber = false; public isString = false; public useAbsoluteValue = false; + public readonly copiedTitle = $localize`The value has been copied to the clipboard`; + public readonly copyToClipboardTitle = $localize`Copy to clipboard`; + public constructor( private changeDetectorRef: ChangeDetectorRef, private clipboard: Clipboard, private snackBar: MatSnackBar ) { addIcons({ + checkmarkOutline, copyOutline }); } public readonly precision = input(); + private copyToClipboardTimeout: ReturnType; + private readonly formatOptions = computed(() => { const digits = this.hasPrecision ? this.precision() : 2; @@ -178,6 +186,18 @@ export class GfValueComponent implements AfterViewInit, OnChanges { public onCopyValueToClipboard() { this.clipboard.copy(String(this.value)); + this.isCopied = true; + + if (this.copyToClipboardTimeout) { + clearTimeout(this.copyToClipboardTimeout); + } + + this.copyToClipboardTimeout = setTimeout(() => { + this.isCopied = false; + + this.changeDetectorRef.markForCheck(); + }, ms('3 seconds')); + this.snackBar.open( '✅ ' + $localize`${this.value} has been copied to the clipboard`, undefined, @@ -187,12 +207,23 @@ export class GfValueComponent implements AfterViewInit, OnChanges { ); } + public ngOnDestroy() { + if (this.copyToClipboardTimeout) { + clearTimeout(this.copyToClipboardTimeout); + } + } + private initializeVariables() { this.absoluteValue = 0; this.formattedValue = ''; + this.isCopied = false; this.isNumber = false; this.isString = false; this.locale = this.locale || getLocale(); this.useAbsoluteValue = false; + + if (this.copyToClipboardTimeout) { + clearTimeout(this.copyToClipboardTimeout); + } } } From b888134056d7a17d0de1fb7987c37b62904cca3a Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:59:45 +0700 Subject: [PATCH 05/18] Task/improve type safety in statistics gathering and tag services (#7328) * feat(api): create interface for BetterStack update SLA response * fix(api): resolve type errors * fix(api): resolve type errors in tag service --- .../interfaces/interfaces.ts | 7 +++ .../statistics-gathering.processor.ts | 46 ++++++++----------- apps/api/src/services/tag/tag.service.ts | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts diff --git a/apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts b/apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts new file mode 100644 index 0000000000..3c316720f3 --- /dev/null +++ b/apps/api/src/services/queues/statistics-gathering/interfaces/interfaces.ts @@ -0,0 +1,7 @@ +export interface BetterStackUptimeSlaResponse { + data: { + attributes: { + availability: number; + }; + }; +} diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts index 82f362d258..21d009805d 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts @@ -25,6 +25,14 @@ import { Injectable, Logger } from '@nestjs/common'; import * as cheerio from 'cheerio'; import { format, subDays } from 'date-fns'; +import { BetterStackUptimeSlaResponse } from './interfaces/interfaces'; + +const GATHER_STATISTICS_CONCURRENCY = parseInt( + process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ?? + DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(), + 10 +); + @Injectable() @Processor(STATISTICS_GATHERING_QUEUE) export class StatisticsGatheringProcessor { @@ -37,11 +45,7 @@ export class StatisticsGatheringProcessor { ) {} @Process({ - concurrency: parseInt( - process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ?? - DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(), - 10 - ), + concurrency: GATHER_STATISTICS_CONCURRENCY, name: GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME }) public async gatherDockerHubPullsStatistics() { @@ -58,11 +62,7 @@ export class StatisticsGatheringProcessor { } @Process({ - concurrency: parseInt( - process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ?? - DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(), - 10 - ), + concurrency: GATHER_STATISTICS_CONCURRENCY, name: GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME }) public async gatherGitHubContributorsStatistics() { @@ -83,11 +83,7 @@ export class StatisticsGatheringProcessor { } @Process({ - concurrency: parseInt( - process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ?? - DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(), - 10 - ), + concurrency: GATHER_STATISTICS_CONCURRENCY, name: GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME }) public async gatherGitHubStargazersStatistics() { @@ -106,11 +102,7 @@ export class StatisticsGatheringProcessor { } @Process({ - concurrency: parseInt( - process.env.PROCESSOR_GATHER_STATISTICS_CONCURRENCY ?? - DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY.toString(), - 10 - ), + concurrency: GATHER_STATISTICS_CONCURRENCY, name: GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME }) public async gatherUptimeStatistics() { @@ -140,14 +132,14 @@ export class StatisticsGatheringProcessor { private async countDockerHubPulls(): Promise { try { - const { pull_count } = (await this.fetchService + const { pull_count } = await this.fetchService .fetch('https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', { headers: { 'User-Agent': 'request' }, signal: AbortSignal.timeout( this.configurationService.get('REQUEST_TIMEOUT') ) }) - .then((res) => res.json())) as { pull_count: number }; + .then<{ pull_count: number }>((res) => res.json()); return pull_count; } catch (error) { @@ -157,7 +149,7 @@ export class StatisticsGatheringProcessor { } } - private async countGitHubContributors(): Promise { + private async countGitHubContributors(): Promise { try { const body = await this.fetchService .fetch('https://github.com/ghostfolio/ghostfolio', { @@ -189,14 +181,14 @@ export class StatisticsGatheringProcessor { private async countGitHubStargazers(): Promise { try { - const { stargazers_count } = (await this.fetchService + const { stargazers_count } = await this.fetchService .fetch('https://api.github.com/repos/ghostfolio/ghostfolio', { headers: { 'User-Agent': 'request' }, signal: AbortSignal.timeout( this.configurationService.get('REQUEST_TIMEOUT') ) }) - .then((res) => res.json())) as { stargazers_count: number }; + .then<{ stargazers_count: number }>((res) => res.json()); return stargazers_count; } catch (error) { @@ -213,7 +205,7 @@ export class StatisticsGatheringProcessor { `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( subDays(new Date(), 90), DATE_FORMAT - )}&to${format(new Date(), DATE_FORMAT)}`, + )}&to=${format(new Date(), DATE_FORMAT)}`, { headers: { [HEADER_KEY_TOKEN]: `Bearer ${this.configurationService.get( @@ -225,7 +217,7 @@ export class StatisticsGatheringProcessor { ) } ) - .then((res) => res.json()); + .then((res) => res.json()); return data.attributes.availability / 100; } catch (error) { diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index 38fb90a696..de052f9a1f 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -20,7 +20,7 @@ export class TagService { public async getTag( tagWhereUniqueInput: Prisma.TagWhereUniqueInput - ): Promise { + ): Promise { return this.prismaService.tag.findUnique({ where: tagWhereUniqueInput }); From 3276cb82c090f90c6439e311fd91dd6126558400 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:02:38 +0200 Subject: [PATCH 06/18] Task/update locales (#7366) Co-authored-by: github-actions[bot] --- apps/client/src/locales/messages.ca.xlf | 10 +++++++--- apps/client/src/locales/messages.de.xlf | 10 +++++++--- apps/client/src/locales/messages.es.xlf | 10 +++++++--- apps/client/src/locales/messages.fr.xlf | 10 +++++++--- apps/client/src/locales/messages.it.xlf | 10 +++++++--- apps/client/src/locales/messages.ja.xlf | 10 +++++++--- apps/client/src/locales/messages.ko.xlf | 10 +++++++--- apps/client/src/locales/messages.nl.xlf | 10 +++++++--- apps/client/src/locales/messages.pl.xlf | 10 +++++++--- apps/client/src/locales/messages.pt.xlf | 10 +++++++--- apps/client/src/locales/messages.tr.xlf | 10 +++++++--- apps/client/src/locales/messages.uk.xlf | 10 +++++++--- apps/client/src/locales/messages.xlf | 10 +++++++--- apps/client/src/locales/messages.zh.xlf | 10 +++++++--- 14 files changed, 98 insertions(+), 42 deletions(-) diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index bed80cfe5f..aa1734e324 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -2545,6 +2545,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -5263,8 +5267,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index a442126694..1aa95c70f7 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -1432,6 +1432,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2353,8 +2357,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7225,7 +7229,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index e05655a981..81f5ce42e6 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -1417,6 +1417,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2338,8 +2342,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7202,7 +7206,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 565f18227b..3637a997f4 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -1800,6 +1800,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2857,8 +2861,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 2735f2d574..26f5752c84 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -1417,6 +1417,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2338,8 +2342,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7202,7 +7206,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.ja.xlf b/apps/client/src/locales/messages.ja.xlf index 538c9f7902..f584352a48 100644 --- a/apps/client/src/locales/messages.ja.xlf +++ b/apps/client/src/locales/messages.ja.xlf @@ -2342,6 +2342,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -4863,8 +4867,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7242,7 +7246,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index 6ef1b7fd5b..6793e720ea 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -2342,6 +2342,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -4855,8 +4859,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7242,7 +7246,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 98fa880799..1f70d55182 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -1416,6 +1416,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2337,8 +2341,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index 026d669747..5d18539aa3 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -2309,6 +2309,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -4822,8 +4826,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index c68fd5da44..80f351a930 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -1800,6 +1800,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -2785,8 +2789,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index b2a1fd72a2..85a1ce506d 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -4246,8 +4246,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -4574,6 +4574,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -7201,7 +7205,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 6e07590bb8..953b05b35c 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -2773,6 +2773,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -5600,7 +5604,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 @@ -5679,8 +5683,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index c1603bca8f..5e26a30cb0 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -2146,6 +2146,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -4429,8 +4433,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -6577,7 +6581,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 7cc82c8d31..be338019d9 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -2318,6 +2318,10 @@ libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 + + libs/ui/src/lib/value/value.component.ts + 63 + Grant access @@ -4839,8 +4843,8 @@ 88 - libs/ui/src/lib/value/value.component.html - 18 + libs/ui/src/lib/value/value.component.ts + 64 @@ -7202,7 +7206,7 @@ libs/ui/src/lib/value/value.component.ts - 182 + 202 From 16394bb4f99d64ec1febf061ed32522331ad7a15 Mon Sep 17 00:00:00 2001 From: KDxGautam Date: Sat, 18 Jul 2026 13:35:19 +0530 Subject: [PATCH 07/18] Bugfix/hover styling of tags selector (#7359) * Fix hover styling of tags selector * Update changelog --- CHANGELOG.md | 4 ++++ .../src/lib/tags-selector/tags-selector.component.html | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7ba56262..90f1110d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the language localization for German (`de`) - Upgraded `stripe` from version `22.2.3` to `22.3.2` +### Fixed + +- Fixed an issue with the delete button in the tags selector component + ## 3.28.0 - 2026-07-17 ### Changed diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.html b/libs/ui/src/lib/tags-selector/tags-selector.component.html index 92ea2b2106..4f9d82ae98 100644 --- a/libs/ui/src/lib/tags-selector/tags-selector.component.html +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.html @@ -32,13 +32,11 @@ } } @for (tag of tagsSelected(); track tag.id) { - + {{ tag.name }} - + } Date: Sat, 18 Jul 2026 10:07:57 +0200 Subject: [PATCH 08/18] Release 3.29.0 (#7368) --- 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 90f1110d44..f1d8ca3b6e 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.29.0 - 2026-07-18 ### Added diff --git a/package-lock.json b/package-lock.json index adb5f9f9d0..ccd0388142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.28.0", + "version": "3.29.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.28.0", + "version": "3.29.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 521d73d996..14d4f5f112 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.28.0", + "version": "3.29.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 37bd17d3c14fdc85abda987f3b18ee3ad77c51d1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:30:09 +0200 Subject: [PATCH 09/18] Task/simplify getHistorical() function response in data provider interface (#7369) * Simplify getHistorical() function response * Update changelog --- CHANGELOG.md | 6 ++++ .../ghostfolio/ghostfolio.service.ts | 4 +-- .../alpha-vantage/alpha-vantage.service.ts | 8 ++--- .../coingecko/coingecko.service.ts | 10 +++---- .../data-provider/data-provider.service.ts | 2 +- .../eod-historical-data.service.ts | 29 +++++++++---------- .../financial-modeling-prep.service.ts | 10 +++---- .../ghostfolio/ghostfolio.service.ts | 6 ++-- .../google-sheets/google-sheets.service.ts | 6 ++-- .../interfaces/data-provider.interface.ts | 4 +-- .../data-provider/manual/manual.service.ts | 17 +++++------ .../rapid-api/rapid-api.service.ts | 8 ++--- .../yahoo-finance/yahoo-finance.service.ts | 8 ++--- 13 files changed, 52 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d8ca3b6e..4e699e6cbb 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 + +### Changed + +- Simplified the `getHistorical()` function response in the data provider interface + ## 3.29.0 - 2026-07-18 ### Added diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts index abbb5cbf67..2c22f0c10f 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts @@ -171,7 +171,7 @@ export class GhostfolioService { try { const promises: Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }>[] = []; for (const dataProviderService of this.getDataProviderServices()) { @@ -185,7 +185,7 @@ export class GhostfolioService { to }) .then((historicalData) => { - result.historicalData = historicalData[symbol]; + result.historicalData = historicalData; return historicalData; }) diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 40b45a115b..799f1280b6 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -70,7 +70,7 @@ export class AlphaVantageService symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { const historicalData: { @@ -83,11 +83,9 @@ export class AlphaVantageService ); const response: { - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; } = {}; - response[symbol] = {}; - for (const [key, timeSeries] of Object.entries( historicalData['Time Series (Digital Currency Daily)'] ).sort()) { @@ -95,7 +93,7 @@ export class AlphaVantageService isAfter(from, parse(key, DATE_FORMAT, new Date())) && isBefore(to, parse(key, DATE_FORMAT, new Date())) ) { - response[symbol][key] = { + response[key] = { marketPrice: parseFloat(timeSeries['4a. close (USD)']) }; } diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index 5d6ed79aac..96bc00561a 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -115,7 +115,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { const queryParams = new URLSearchParams({ @@ -143,13 +143,11 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit { } const result: { - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; - } = { - [symbol]: {} - }; + [date: string]: DataProviderHistoricalResponse; + } = {}; for (const [timestamp, marketPrice] of prices) { - result[symbol][format(fromUnixTime(timestamp / 1000), DATE_FORMAT)] = { + result[format(fromUnixTime(timestamp / 1000), DATE_FORMAT)] = { marketPrice }; } 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 35dc97d2fd..49f5f68f44 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -508,7 +508,7 @@ export class DataProviderService implements OnModuleInit { requestTimeout: ms('30 seconds') }) .then((data) => { - return { dataSource, symbol, data: data?.[symbol] }; + return { data, dataSource, symbol }; }) ); } diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index ebb6cd743c..6bc003fc23 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -147,7 +147,7 @@ export class EodHistoricalDataService symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { symbol = this.convertToEodSymbol(symbol); @@ -166,22 +166,19 @@ export class EodHistoricalDataService }) .then((res) => res.json()); - return response.reduce( - (result, { adjusted_close, date }) => { - if (isNumber(adjusted_close)) { - result[this.convertFromEodSymbol(symbol)][date] = { - marketPrice: adjusted_close - }; - } else { - this.logger.error( - `Could not get historical market data for ${symbol} (${this.getName()}) at ${date}` - ); - } + return response.reduce((result, { adjusted_close, date }) => { + if (isNumber(adjusted_close)) { + result[date] = { + marketPrice: adjusted_close + }; + } else { + this.logger.error( + `Could not get historical market data for ${symbol} (${this.getName()}) at ${date}` + ); + } - return result; - }, - { [this.convertFromEodSymbol(symbol)]: {} } - ); + return result; + }, {}); } catch (error) { throw new Error( `Could not get historical market data for ${symbol} (${this.getName()}) from ${format( diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 4e3502033b..83e7fb6c2c 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -336,14 +336,12 @@ export class FinancialModelingPrepService symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { const MAX_YEARS_PER_REQUEST = 5; const result: { - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; - } = { - [symbol]: {} - }; + [date: string]: DataProviderHistoricalResponse; + } = {}; let currentFrom = from; @@ -378,7 +376,7 @@ export class FinancialModelingPrepService isAfter(parseDate(date), currentFrom)) && isBefore(parseDate(date), currentTo) ) { - result[symbol][date] = { + result[date] = { marketPrice: close }; } diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index 81b997037b..5b59e9a006 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -172,7 +172,7 @@ export class GhostfolioService implements DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { const queryParams = new URLSearchParams({ @@ -198,9 +198,7 @@ export class GhostfolioService implements DataProviderInterface { const { historicalData } = (await response.json()) as HistoricalResponse; - return { - [symbol]: historicalData - }; + return historicalData; } catch (error) { if (error?.status === StatusCodes.TOO_MANY_REQUESTS) { error.name = 'RequestError'; diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index 13f671bd43..75fae673e9 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -60,7 +60,7 @@ export class GoogleSheetsService implements DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { const sheet = await this.getSheet({ @@ -85,9 +85,7 @@ export class GoogleSheetsService implements DataProviderInterface { historicalData[format(date, DATE_FORMAT)] = { marketPrice: close }; }); - return { - [symbol]: historicalData - }; + return historicalData; } catch (error) { throw new Error( `Could not get historical market data for ${symbol} (${this.getName()}) from ${format( diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index 8c2fb64d8b..1e6d2496b0 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts @@ -35,8 +35,8 @@ export interface DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; - }>; // TODO: Return only one symbol + [date: string]: DataProviderHistoricalResponse; + }>; getMarketDataOfMarkets?({ includeHistoricalData, diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index 66571c2397..a8cfa8b0b5 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -79,7 +79,7 @@ export class ManualService implements DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( @@ -90,14 +90,13 @@ export class ManualService implements DataProviderInterface { if (defaultMarketPrice) { const historical: { - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; - } = { - [symbol]: {} - }; + [date: string]: DataProviderHistoricalResponse; + } = {}; + let date = from; while (isBefore(date, to)) { - historical[symbol][format(date, DATE_FORMAT)] = { + historical[format(date, DATE_FORMAT)] = { marketPrice: defaultMarketPrice }; @@ -115,10 +114,8 @@ export class ManualService implements DataProviderInterface { }); return { - [symbol]: { - [format(getYesterday(), DATE_FORMAT)]: { - marketPrice: value - } + [format(getYesterday(), DATE_FORMAT)]: { + marketPrice: value } }; } catch (error) { 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 e704f28618..9af22b79a9 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 @@ -58,7 +58,7 @@ export class RapidApiService implements DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { try { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { @@ -66,10 +66,8 @@ export class RapidApiService implements DataProviderInterface { if (fgi) { return { - [symbol]: { - [format(getYesterday(), DATE_FORMAT)]: { - marketPrice: fgi.previousClose.value - } + [format(getYesterday(), DATE_FORMAT)]: { + marketPrice: fgi.previousClose.value } }; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 93949ebc05..52fb5d5a81 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -123,7 +123,7 @@ export class YahooFinanceService implements DataProviderInterface { symbol, to }: GetHistoricalParams): Promise<{ - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; }> { if (isSameDay(from, to)) { to = addDays(to, 1); @@ -144,13 +144,11 @@ export class YahooFinanceService implements DataProviderInterface { ); const response: { - [symbol: string]: { [date: string]: DataProviderHistoricalResponse }; + [date: string]: DataProviderHistoricalResponse; } = {}; - response[symbol] = {}; - for (const historicalItem of historicalResult) { - response[symbol][format(historicalItem.date, DATE_FORMAT)] = { + response[format(historicalItem.date, DATE_FORMAT)] = { marketPrice: historicalItem.close }; } 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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",