diff --git a/CHANGELOG.md b/CHANGELOG.md index 7936c1cf7..26f396788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `prettier` from version `3.8.3` to `3.8.4` -## 3.15.0 - 2026-06-23 +## 3.16.0 - 2026-06-24 + +### Added + +- Extended the user account settings with a copy-to-clipboard button for the user id +- Added pagination to the platform management of the admin control panel +- Added pagination to the tag management of the admin control panel +- Extended the asset profile details dialog of the admin control panel with a copy-to-clipboard button for the ISIN number +- Extended the asset profile details dialog of the admin control panel with a copy-to-clipboard button for the symbol + +### Changed + +- Improved the throughput of the market data gathering queue by applying the rate limit per data source +- Decreased the rate limiter duration of the market data gathering queue jobs from 4 to 3 seconds +- Removed the deprecated `SymbolProfile` field from the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol` +- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.2.2` to `13.3` + +### Fixed + +- Fixed an issue with hourly market data updates not refreshing prices for asset profiles with `MANUAL` data source +- Fixed an issue with the log context formatting in the performance logging service + +## 3.15.1 - 2026-06-23 ### Changed @@ -24,6 +46,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..41697b346 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; } @@ -957,7 +957,6 @@ export class PortfolioService { marketPrice, marketPriceMax, marketPriceMin, - SymbolProfile, tags, assetProfile: { assetClass: SymbolProfile.assetClass, diff --git a/apps/api/src/interceptors/performance-logging/performance-logging.service.ts b/apps/api/src/interceptors/performance-logging/performance-logging.service.ts index a07783cd9..b2ea03c37 100644 --- a/apps/api/src/interceptors/performance-logging/performance-logging.service.ts +++ b/apps/api/src/interceptors/performance-logging/performance-logging.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; @Injectable() export class PerformanceLoggingService { - private readonly logger = new Logger(PerformanceLoggingService.name); + private readonly logger = new Logger(); public logPerformance({ className, 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); 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/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts index 5ac6c40c0..d66411797 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts @@ -28,7 +28,8 @@ import { DataGatheringProcessor } from './data-gathering.processor'; }), BullModule.registerQueue({ limiter: { - duration: ms('4 seconds'), + duration: ms('3 seconds'), + groupKey: 'dataSource', max: 1 }, name: DATA_GATHERING_QUEUE 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 f00c279a0..9b69ef6fc 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 @@ -165,7 +165,11 @@ } @else {
- Symbol
@@ -202,6 +206,7 @@
ISIN + + diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts index 26e6c2b1e..727585478 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts @@ -1,4 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { getLocale, getLowercase } from '@ghostfolio/common/helper'; @@ -22,6 +23,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; @@ -46,6 +48,7 @@ import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform- IonIcon, MatButtonModule, MatMenuModule, + MatPaginatorModule, MatSortModule, MatTableModule, RouterModule @@ -59,11 +62,13 @@ export class GfAdminPlatformComponent implements OnInit { protected dataSource = new MatTableDataSource(); protected readonly displayedColumns = ['name', 'url', 'accounts', 'actions']; + protected readonly pageSize = DEFAULT_PAGE_SIZE; protected platforms: Platform[]; private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); + private readonly paginator = viewChild.required(MatPaginator); private readonly sort = viewChild.required(MatSort); private readonly adminService = inject(AdminService); @@ -145,6 +150,7 @@ export class GfAdminPlatformComponent implements OnInit { this.platforms = platforms; this.dataSource = new MatTableDataSource(platforms); + this.dataSource.paginator = this.paginator(); this.dataSource.sort = this.sort(); this.dataSource.sortingDataAccessor = getLowercase; 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; } } diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html index 3c125d5c0..a84cbb283 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.html +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html @@ -89,3 +89,9 @@ + + diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts index fcb901707..7bd69c964 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts @@ -1,4 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { getLocale, getLowercase } from '@ghostfolio/common/helper'; @@ -21,6 +22,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; @@ -44,6 +46,7 @@ import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/int IonIcon, MatButtonModule, MatMenuModule, + MatPaginatorModule, MatSortModule, MatTableModule, RouterModule @@ -62,11 +65,13 @@ export class GfAdminTagComponent implements OnInit { 'activities', 'actions' ]; + protected readonly pageSize = DEFAULT_PAGE_SIZE; protected tags: Tag[]; private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); + private readonly paginator = viewChild.required(MatPaginator); private readonly sort = viewChild.required(MatSort); private readonly changeDetectorRef = inject(ChangeDetectorRef); @@ -147,6 +152,7 @@ export class GfAdminTagComponent implements OnInit { this.tags = tags; this.dataSource = new MatTableDataSource(this.tags); + this.dataSource.paginator = this.paginator(); this.dataSource.sort = this.sort(); this.dataSource.sortingDataAccessor = getLowercase; diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index e14c638be..6fa3a4853 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -116,6 +116,19 @@ export class GfHoldingDetailDialogComponent implements OnInit { protected accounts: Account[]; protected activitiesCount: number; protected assetClass: string; + protected assetProfile: Pick< + EnhancedSymbolProfile, + | 'assetClass' + | 'assetSubClass' + | 'countries' + | 'currency' + | 'dataSource' + | 'isin' + | 'name' + | 'sectors' + | 'symbol' + | 'userId' + >; protected assetSubClass: string; protected averagePrice: number; protected averagePricePrecision = 2; @@ -164,7 +177,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { }; protected sortColumn = 'date'; protected sortDirection: SortDirection = 'desc'; - protected SymbolProfile: EnhancedSymbolProfile; protected tagsAvailable: Tag[]; protected readonly translate = translate; protected user: User; @@ -266,6 +278,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { .subscribe( ({ activitiesCount, + assetProfile, averagePrice, dataProviderInfo, dateOfFirstActivity, @@ -280,11 +293,11 @@ export class GfHoldingDetailDialogComponent implements OnInit { netPerformancePercentWithCurrencyEffect, netPerformanceWithCurrencyEffect, quantity, - SymbolProfile, tags, value }) => { this.activitiesCount = activitiesCount; + this.assetProfile = assetProfile; this.averagePrice = averagePrice; if ( @@ -322,8 +335,8 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.user?.permissions, permissions.readMarketDataOfOwnAssetProfile ) && - SymbolProfile?.dataSource === 'MANUAL' && - SymbolProfile?.userId === this.user?.id; + assetProfile?.dataSource === 'MANUAL' && + assetProfile?.userId === this.user?.id; this.historicalDataItems = historicalData.map( ({ averagePrice, date, marketPrice }) => { @@ -402,7 +415,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { if (Number.isInteger(this.quantity)) { this.quantityPrecision = 0; - } else if (SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') { + } else if (assetProfile?.assetSubClass === 'CRYPTOCURRENCY') { if (this.quantity < 10) { this.quantityPrecision = 8; } else if (this.quantity < 1000) { @@ -413,7 +426,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { } this.sectors = {}; - this.SymbolProfile = SymbolProfile; this.tags = tags.map((tag) => { return { @@ -427,21 +439,21 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.value = value; const reportDataGlitchSubject = `Ghostfolio Data Glitch Report${ - this.SymbolProfile?.symbol ? ` (${this.SymbolProfile.symbol})` : '' + this.assetProfile?.symbol ? ` (${this.assetProfile.symbol})` : '' }`; - this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.SymbolProfile?.symbol}%0DData Source: ${this.SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; + this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; - if (this.SymbolProfile?.assetClass) { - this.assetClass = translate(this.SymbolProfile?.assetClass); + if (this.assetProfile?.assetClass) { + this.assetClass = translate(this.assetProfile?.assetClass); } - if (this.SymbolProfile?.assetSubClass) { - this.assetSubClass = translate(this.SymbolProfile?.assetSubClass); + if (this.assetProfile?.assetSubClass) { + this.assetSubClass = translate(this.assetProfile?.assetSubClass); } - if (this.SymbolProfile?.countries?.length > 0) { - for (const country of this.SymbolProfile.countries) { + if (this.assetProfile?.countries?.length > 0) { + for (const country of this.assetProfile.countries) { this.countries[country.code] = { name: getCountryName({ code: country.code }), value: country.weight @@ -449,8 +461,8 @@ export class GfHoldingDetailDialogComponent implements OnInit { } } - if (this.SymbolProfile?.sectors?.length > 0) { - for (const sector of this.SymbolProfile.sectors) { + if (this.assetProfile?.sectors?.length > 0) { + for (const sector of this.assetProfile.sectors) { this.sectors[sector.name] = { name: translate(sector.name), value: sector.weight @@ -570,12 +582,12 @@ export class GfHoldingDetailDialogComponent implements OnInit { const activity: CreateOrderDto = { accountId: this.accounts.length === 1 ? this.accounts[0].id : undefined, comment: undefined, - currency: this.SymbolProfile?.currency ?? '', - dataSource: this.SymbolProfile?.dataSource, + currency: this.assetProfile?.currency ?? '', + dataSource: this.assetProfile?.dataSource, date: today.toISOString(), fee: 0, quantity: this.quantity, - symbol: this.SymbolProfile?.symbol ?? '', + symbol: this.assetProfile?.symbol ?? '', tags: this.tags.map(({ id }) => { return id; }), @@ -606,7 +618,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { .subscribe((data) => { downloadAsFile({ content: data, - fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format( + fileName: `ghostfolio-export-${this.assetProfile?.symbol}-${format( parseISO(data.meta.date), 'yyyyMMddHHmm' )}.json`, diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 0d5691874..ff1d19cc3 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1,7 +1,7 @@ @@ -24,11 +24,11 @@ [benchmarkDataItems]="benchmarkDataItems" [benchmarkLabel]="benchmarkLabel" [colorScheme]="data.colorScheme" - [currency]="SymbolProfile?.currency" + [currency]="assetProfile?.currency" [historicalDataItems]="historicalDataItems" [isAnimated]="true" [label]=" - isUUID(data.symbol) ? (SymbolProfile?.name ?? data.symbol) : data.symbol + isUUID(data.symbol) ? (assetProfile?.name ?? data.symbol) : data.symbol " [locale]="data.locale" [showGradient]="true" @@ -60,8 +60,8 @@ [value]="netPerformanceWithCurrencyEffect" > @if ( - SymbolProfile?.currency && - data.baseCurrency !== SymbolProfile?.currency + assetProfile?.currency && + data.baseCurrency !== assetProfile?.currency ) { Change with currency effect } @else { @@ -80,8 +80,8 @@ [value]="netPerformancePercentWithCurrencyEffect" > @if ( - SymbolProfile?.currency && - data.baseCurrency !== SymbolProfile?.currency + assetProfile?.currency && + data.baseCurrency !== assetProfile?.currency ) { Performance with currency effect } @else { @@ -96,7 +96,7 @@ [isCurrency]="true" [locale]="data.locale" [precision]="averagePricePrecision" - [unit]="SymbolProfile?.currency" + [unit]="assetProfile?.currency" [value]="averagePrice" >Average Unit Price @@ -108,7 +108,7 @@ [isCurrency]="true" [locale]="data.locale" [precision]="marketPricePrecision" - [unit]="SymbolProfile?.currency" + [unit]="assetProfile?.currency" [value]="marketPrice" >Market Price @@ -124,7 +124,7 @@ [isCurrency]="true" [locale]="data.locale" [precision]="marketPriceMinPrecision" - [unit]="SymbolProfile?.currency" + [unit]="assetProfile?.currency" [value]="marketPriceMin" >Minimum Price @@ -140,7 +140,7 @@ [isCurrency]="true" [locale]="data.locale" [precision]="marketPriceMaxPrecision" - [unit]="SymbolProfile?.currency" + [unit]="assetProfile?.currency" [value]="marketPriceMax" >Maximum Price @@ -250,23 +250,23 @@ >
@if ( - SymbolProfile?.countries?.length > 0 || - SymbolProfile?.sectors?.length > 0 + assetProfile?.countries?.length > 0 || + assetProfile?.sectors?.length > 0 ) { @if ( - SymbolProfile?.countries?.length === 1 && - SymbolProfile?.sectors?.length === 1 + assetProfile?.countries?.length === 1 && + assetProfile?.sectors?.length === 1 ) {
Sector
- @if (SymbolProfile?.countries?.length === 1) { + @if (assetProfile?.countries?.length === 1) {
CountrySymbol
@@ -322,8 +322,8 @@ ISIN @@ -404,12 +404,12 @@
Market Data
@@ -462,8 +462,8 @@ mat-stroked-button [queryParams]="{ assetProfileDialog: true, - dataSource: SymbolProfile?.dataSource, - symbol: SymbolProfile?.symbol + dataSource: assetProfile?.dataSource, + symbol: assetProfile?.symbol }" [routerLink]="routerLinkAdminControlMarketData" (click)="onClose()" diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index 72bbfc2c6..0101bb90a 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -12,6 +12,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { DataService } from '@ghostfolio/ui/services'; +import { GfValueComponent } from '@ghostfolio/ui/value'; import { ChangeDetectionStrategy, @@ -51,6 +52,7 @@ import { catchError } from 'rxjs/operators'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [ FormsModule, + GfValueComponent, IonIcon, MatButtonModule, MatCardModule, diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index f646ef0fd..e4e9777de 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -260,7 +260,13 @@
Ghostfolio User ID
-
{{ user?.id }}
+
+ +
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index e6d717c7b..03600088f 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -90,9 +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 = + 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', diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts index 3b07666c9..075234e6a 100644 --- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts @@ -44,10 +44,6 @@ export interface PortfolioHoldingResponse { netPerformanceWithCurrencyEffect: number; performances: Benchmark['performances']; quantity: number; - - /* @deprecated */ - SymbolProfile: EnhancedSymbolProfile; - tags: Tag[]; value: number; } diff --git a/package-lock.json b/package-lock.json index c1f3310dc..c55ff5ef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.16.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -43,8 +43,8 @@ "@openrouter/ai-sdk-provider": "2.9.1", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", - "@simplewebauthn/browser": "13.2.2", - "@simplewebauthn/server": "13.2.2", + "@simplewebauthn/browser": "13.3.0", + "@simplewebauthn/server": "13.3.1", "ai": "6.0.174", "alphavantage": "2.2.0", "big.js": "7.0.1", @@ -129,7 +129,7 @@ "@storybook/addon-themes": "10.1.10", "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "6.0.2", - "@types/big.js": "6.2.2", + "@types/big.js": "7.0.0", "@types/cookie-parser": "1.4.10", "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5", @@ -13296,25 +13296,25 @@ } }, "node_modules/@simplewebauthn/browser": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", - "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", "license": "MIT" }, "node_modules/@simplewebauthn/server": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz", - "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==", + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.1.tgz", + "integrity": "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.3.10", - "@peculiar/asn1-ecc": "^2.3.8", - "@peculiar/asn1-rsa": "^2.3.8", - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/asn1-x509": "^2.3.8", - "@peculiar/x509": "^1.13.0" + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/x509": "^1.14.3" }, "engines": { "node": ">=20.0.0" @@ -14020,9 +14020,9 @@ } }, "node_modules/@types/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-7.0.0.tgz", + "integrity": "sha512-WfAGp7IbJvyB8EmWK4tJD24rJRAL6uVbw3LV/hJntFNam+os9KWKj0PzXo8rRRpjupYK8U0M8FoBB8dBhWF2dg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 6fe206f01..dee237dd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.15.0", + "version": "3.16.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -87,8 +87,8 @@ "@openrouter/ai-sdk-provider": "2.9.1", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", - "@simplewebauthn/browser": "13.2.2", - "@simplewebauthn/server": "13.2.2", + "@simplewebauthn/browser": "13.3.0", + "@simplewebauthn/server": "13.3.1", "ai": "6.0.174", "alphavantage": "2.2.0", "big.js": "7.0.1", @@ -173,7 +173,7 @@ "@storybook/addon-themes": "10.1.10", "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "6.0.2", - "@types/big.js": "6.2.2", + "@types/big.js": "7.0.0", "@types/cookie-parser": "1.4.10", "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5",