From 2b5d2701bd00acb4fff83e1ebf2f5b19b42dfeb9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:06:01 +0200 Subject: [PATCH 01/15] Task/improve exchange rate and market data gathering robustness (#7119) * Improve exchange rate robustness * Improve market data gathering robustness * Update changelog --- CHANGELOG.md | 2 + .../src/app/activities/activities.service.ts | 8 +- apps/api/src/app/import/import.service.ts | 15 ++-- .../src/app/portfolio/portfolio.service.ts | 8 +- .../market-data/market-data.service.ts | 74 ++++++++++--------- libs/common/src/lib/config.ts | 1 + 6 files changed, 58 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f175878fc..c8d6a7244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue where symbols with special characters caused API request failures by URL encoding the symbol - Fixed the disabled state of the delete action in the asset profiles actions menu of the historical market data table in the admin control panel - Fixed the persistence of an empty `locale` string in the scraper configuration +- Fixed a transaction timeout that prevented gathering historical market data for symbols with a long history +- Fixed an exception in various portfolio endpoints when historical exchange rate data is missing ## 3.14.0 - 2026-06-22 diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index f57507e3d..44ab67e45 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -746,10 +746,10 @@ export class ActivitiesService { const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); const [ - feeInAssetProfileCurrency, - feeInBaseCurrency, - unitPriceInAssetProfileCurrency, - valueInBaseCurrency + feeInAssetProfileCurrency = 0, + feeInBaseCurrency = 0, + unitPriceInAssetProfileCurrency = 0, + valueInBaseCurrency = 0 ] = await Promise.all([ this.exchangeRateDataService.toCurrencyAtDate( order.fee, diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 2ecc4d3a5..ba704d5ad 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -592,18 +592,19 @@ export class ImportService { const value = new Big(quantity).mul(unitPrice).toNumber(); - const valueInBaseCurrency = this.exchangeRateDataService.toCurrencyAtDate( - value, - currency ?? assetProfile.currency, - userCurrency, - date - ); + const valueInBaseCurrency = + (await this.exchangeRateDataService.toCurrencyAtDate( + value, + currency ?? assetProfile.currency, + userCurrency, + date + )) ?? 0; activities.push({ ...order, error, value, - valueInBaseCurrency: await valueInBaseCurrency, + valueInBaseCurrency, // @ts-ignore SymbolProfile: assetProfile }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 418e60401..77cbb5b49 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -205,21 +205,21 @@ export class PortfolioService { switch (type) { case ActivityType.DIVIDEND: dividendInBaseCurrency += - await this.exchangeRateDataService.toCurrencyAtDate( + (await this.exchangeRateDataService.toCurrencyAtDate( new Big(quantity).mul(unitPrice).toNumber(), currency ?? SymbolProfile.currency, userCurrency, date - ); + )) ?? 0; break; case ActivityType.INTEREST: interestInBaseCurrency += - await this.exchangeRateDataService.toCurrencyAtDate( + (await this.exchangeRateDataService.toCurrencyAtDate( unitPrice, currency ?? SymbolProfile.currency, userCurrency, date - ); + )) ?? 0; break; } 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 27c741055..086434724 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -1,6 +1,7 @@ import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT } from '@ghostfolio/common/config'; import { UpdateMarketDataDto } from '@ghostfolio/common/dtos'; import { resetHours } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; @@ -155,49 +156,52 @@ export class MarketDataService { dataSource, symbol }: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) { - await this.prismaService.$transaction(async (prisma) => { - if (data.length > 0) { - let minTime = Infinity; - let maxTime = -Infinity; + await this.prismaService.$transaction( + async (prisma) => { + if (data.length > 0) { + let minTime = Infinity; + let maxTime = -Infinity; - for (const { date } of data) { - const time = (date as Date).getTime(); + for (const { date } of data) { + const time = (date as Date).getTime(); - if (time < minTime) { - minTime = time; - } + if (time < minTime) { + minTime = time; + } - if (time > maxTime) { - maxTime = time; + if (time > maxTime) { + maxTime = time; + } } - } - const minDate = new Date(minTime); - const maxDate = new Date(maxTime); + const minDate = new Date(minTime); + const maxDate = new Date(maxTime); - await prisma.marketData.deleteMany({ - where: { - dataSource, - symbol, - date: { - gte: minDate, - lte: maxDate + await prisma.marketData.deleteMany({ + where: { + dataSource, + symbol, + date: { + gte: minDate, + lte: maxDate + } } - } - }); + }); - await prisma.marketData.createMany({ - data: data.map(({ date, marketPrice, state }) => ({ - dataSource, - symbol, - date: date as Date, - marketPrice: marketPrice as number, - state: state as MarketDataState - })), - skipDuplicates: true - }); - } - }); + await prisma.marketData.createMany({ + data: data.map(({ date, marketPrice, state }) => ({ + dataSource, + symbol, + date: date as Date, + marketPrice: marketPrice as number, + state: state as MarketDataState + })), + skipDuplicates: true + }); + } + }, + { timeout: DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT } + ); } public async updateAssetProfileIdentifier( diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index e6d717c7b..f96a934e7 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -90,6 +90,7 @@ export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PORT = 3333; export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; +export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT = 60000; export const DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; From 5cd81192ee09e83f4f64ab2298ce9cff4e6faeea Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:07:34 +0200 Subject: [PATCH 02/15] Release 3.15.1 (#7120) --- 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 c8d6a7244..a903122d4 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). -## 3.15.0 - 2026-06-23 +## 3.15.1 - 2026-06-23 ### Changed diff --git a/package-lock.json b/package-lock.json index d3250d5d0..e40bcbe1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.15.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.15.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 423b64cb9..df644f017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.15.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 020c982e73c706c7ec0a81fabe75ef0c73dad7fe Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:05:11 +0200 Subject: [PATCH 03/15] Bugfix/hourly market data updates not refreshing prices for asset profiles with manual data source (#7122) * Fix hourly market data updates not refreshing prices for MANUAL asset profiles * Update changelog --- CHANGELOG.md | 6 +++ .../data-provider/data-provider.service.ts | 6 ++- .../interfaces/data-provider.interface.ts | 1 + .../data-provider/manual/manual.service.ts | 45 ++++++++++--------- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a903122d4..4ec1539da 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 + +### Fixed + +- Fixed an issue with hourly market data updates not refreshing prices for asset profiles with `MANUAL` data source + ## 3.15.1 - 2026-06-23 ### Changed 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 5d49848fa..10b0e6fd8 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -662,7 +662,11 @@ export class DataProviderService implements OnModuleInit { ); const promise = Promise.resolve( - dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk }) + dataProvider.getQuotes({ + requestTimeout, + useCache, + symbols: symbolsChunk + }) ); promises.push( 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 a55c9f328..5002fa87e 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 @@ -75,6 +75,7 @@ export interface GetHistoricalParams { export interface GetQuotesParams { requestTimeout?: number; symbols: string[]; + useCache?: boolean; } export interface GetSearchParams { 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 87e116dda..66571c239 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -136,7 +136,8 @@ export class ManualService implements DataProviderInterface { } public async getQuotes({ - symbols + symbols, + useCache = true }: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> { const response: { [symbol: string]: DataProviderResponse } = {}; @@ -164,32 +165,32 @@ export class ManualService implements DataProviderInterface { } }); - const symbolProfilesWithScraperConfigurationAndInstantMode = - symbolProfiles.filter(({ scraperConfiguration }) => { + const symbolProfilesToScrape = symbolProfiles.filter( + ({ scraperConfiguration }) => { return ( - scraperConfiguration?.mode === 'instant' && + (scraperConfiguration?.mode === 'instant' || !useCache) && scraperConfiguration?.selector && scraperConfiguration?.url ); - }); - - const scraperResultPromises = - symbolProfilesWithScraperConfigurationAndInstantMode.map( - async ({ scraperConfiguration, symbol }) => { - try { - const marketPrice = await this.scrape({ - scraperConfiguration, - symbol - }); - return { marketPrice, symbol }; - } catch (error) { - this.logger.error( - `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}` - ); - return { symbol, marketPrice: undefined }; - } + } + ); + + const scraperResultPromises = symbolProfilesToScrape.map( + async ({ scraperConfiguration, symbol }) => { + try { + const marketPrice = await this.scrape({ + scraperConfiguration, + symbol + }); + return { marketPrice, symbol }; + } catch (error) { + this.logger.error( + `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}` + ); + return { symbol, marketPrice: undefined }; } - ); + } + ); // Wait for all scraping requests to complete concurrently const scraperResults = await Promise.all(scraperResultPromises); From 11f37f8f7ddae1eedd91c361ea8d5d7f697249ba Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:06:26 +0200 Subject: [PATCH 04/15] Task/improve style of Ghostfolio Premium card in admin settings (#7127) Improve style --- .../admin-settings.component.scss | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.scss b/apps/client/src/app/components/admin-settings/admin-settings.component.scss index 1c0a17624..29340ea0d 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.scss +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.scss @@ -10,7 +10,33 @@ } .mat-mdc-card { - --mat-card-outlined-container-color: whitesmoke; + --gradient-opacity: 50%; + --mat-card-outlined-outline-color: rgb(193, 219, 254); + + background-color: white; + background-image: linear-gradient( + 109.6deg, + color-mix(in srgb, rgba(255, 255, 255, 1) 100%, transparent) 11.2%, + color-mix( + in srgb, + rgba(221, 108, 241, 0.26) var(--gradient-opacity), + transparent + ) + 42%, + color-mix( + in srgb, + rgba(229, 106, 253, 0.71) var(--gradient-opacity), + transparent + ) + 71.5%, + color-mix( + in srgb, + rgba(123, 183, 253, 1) var(--gradient-opacity), + transparent + ) + 100% + ); + color: rgb(var(--dark-primary-text)); .mat-mdc-card-actions { min-height: 0; @@ -32,6 +58,6 @@ :host-context(.theme-dark) { .mat-mdc-card { - --mat-card-outlined-container-color: #222222; + --mat-card-outlined-outline-color: white; } } From a2dbfcf8e301cd8b4ed5a70c13ba019d536820e9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:07:04 +0200 Subject: [PATCH 05/15] Task/refactor timeouts to use ms() (#7123) Refactor to use ms() --- libs/common/src/lib/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index f96a934e7..03600088f 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -90,10 +90,12 @@ export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PORT = 3333; export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; -export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT = 60000; +export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT = + ms('1 minute'); export const DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; -export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; +export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = + ms('30 seconds'); export const DEFAULT_REDACTED_PATHS = [ 'accounts[*].balance', From f64edb6705aa86cedb968567603fc669f1cb9371 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:16:54 +0200 Subject: [PATCH 06/15] Feature/add pagination to platform and tag management of admin control panel (#7126) * Add pagination * Update changelog --- CHANGELOG.md | 5 +++++ .../components/admin-platform/admin-platform.component.html | 6 ++++++ .../components/admin-platform/admin-platform.component.ts | 6 ++++++ .../src/app/components/admin-tag/admin-tag.component.html | 6 ++++++ .../src/app/components/admin-tag/admin-tag.component.ts | 6 ++++++ 5 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec1539da..47c43d38f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added pagination to the platform management of the admin control panel +- Added pagination to the tag management of the admin control panel + ### Fixed - Fixed an issue with hourly market data updates not refreshing prices for asset profiles with `MANUAL` data source diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.html b/apps/client/src/app/components/admin-platform/admin-platform.component.html index 44f5a6eab..19682bdc0 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.html +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -96,3 +96,9 @@