From f5b7e5f81aefde4af3a588a8196f91678f28462c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:52:16 +0200 Subject: [PATCH 1/8] Task/update OSS Friends (#7840) Remove OpenBB --- apps/client/src/assets/oss-friends.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/client/src/assets/oss-friends.json b/apps/client/src/assets/oss-friends.json index da409ca2f9..2a49af6ffd 100644 --- a/apps/client/src/assets/oss-friends.json +++ b/apps/client/src/assets/oss-friends.json @@ -1,5 +1,5 @@ { - "createdAt": "2026-05-26T00:00:00.000Z", + "createdAt": "2026-09-07T00:00:00.000Z", "data": [ { "name": "Activepieces", @@ -76,11 +76,6 @@ "description": "Onyx is the open-source AI chat connected to your docs, apps, and people.", "href": "https://onyx.app" }, - { - "name": "OpenBB", - "description": "Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.", - "href": "https://openbb.co" - }, { "name": "OpenStatus", "description": "Open-source monitoring platform with beautiful status pages", From 3e94e7f07a4befb443b03515f09fc154241804ac Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:23:31 +0200 Subject: [PATCH 2/8] Task/improve loading state of activity count in portfolio summary component (#7841) * Improve loading state * Update changelog --- CHANGELOG.md | 1 + .../portfolio-summary.component.html | 59 +++++++++++-------- .../portfolio-summary.component.ts | 11 +++- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4275e2d61..46214cfba6 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 +- Improved the loading state of the activity count in the portfolio summary - Upgraded `zod` from version `4.4.3` to `4.5.4` ## 3.68.0 - 2026-09-06 diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 674fb76eb1..ea408c17d8 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -9,25 +9,33 @@ /> -
-
- {{ summary?.activityCount }} - {summary?.activityCount, plural, - =1 {activity} - other {activities} - } - - - + @if (isLoading) { +
+
+ +
-
+ } @else { +
+
+ {{ summary?.activityCount }} + {summary?.activityCount, plural, + =1 {activity} + other {activities} + } + + + +
+
+ }

@@ -127,11 +135,16 @@
Net Performance - (ROAI) + @if ( + user?.settings?.performanceCalculationType === + PerformanceCalculationType.ROAI + ) { + (ROAI) + }
Date: Mon, 7 Sep 2026 20:43:49 +0200 Subject: [PATCH 3/8] Task/include closed holdings by default in holdings endpoint (#7843) * Return active and closed holdings in holdings endpoint * Update changelog --- CHANGELOG.md | 1 + .../app/portfolio/portfolio.service.spec.ts | 136 +++++++++++++++++- .../src/app/portfolio/portfolio.service.ts | 37 +++-- .../account-detail-dialog.component.ts | 8 +- .../home-holdings/home-holdings.component.ts | 4 +- ...reate-or-update-access-dialog.component.ts | 4 +- ...ate-or-update-activity-dialog.component.ts | 4 +- .../import-activities-dialog.component.ts | 4 + .../analysis/analysis-page.component.ts | 5 +- .../src/lib/assistant/assistant.component.ts | 4 +- 10 files changed, 180 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46214cfba6..dd97e5e31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the loading state of the activity count in the portfolio summary +- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types - Upgraded `zod` from version `4.4.3` to `4.5.4` ## 3.68.0 - 2026-09-06 diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index aed14e090f..0882ff7e64 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -355,8 +355,13 @@ describe('PortfolioService', () => { describe('getDetails', () => { const setUpCashOnlyPortfolio = ({ baseCurrency = 'CHF', - emergencyFund - }: { baseCurrency?: string; emergencyFund?: number } = {}) => { + emergencyFund, + quantity = 2000 + }: { + baseCurrency?: string; + emergencyFund?: number; + quantity?: number; + } = {}) => { const cashAccount: AccountWithBalance = { balance: 2000, comment: null, @@ -421,7 +426,7 @@ describe('PortfolioService', () => { netPerformancePercentage: new Big(0), netPerformancePercentageWithCurrencyEffectMap: {}, netPerformanceWithCurrencyEffectMap: {}, - quantity: new Big(2000), + quantity: new Big(quantity), symbol: 'USD', tags: [], timeWeightedInvestment: new Big(0), @@ -493,6 +498,131 @@ describe('PortfolioService', () => { expect(holdings[0].assetProfile.symbol).toBe('USD'); expect(holdings[0].valueInBaseCurrency).toBe(1000); }); + + it('should include closed holdings when all holdings are requested', async () => { + setUpCashOnlyPortfolio({ quantity: 0 }); + + const { holdings } = await portfolioService.getDetails({ + filters: [], + includeAllHoldings: true, + userId: userDummyData.id + }); + + expect(holdings).toHaveLength(1); + expect(holdings[0].quantity).toBe(0); + }); + + it.each([ + { holdingType: 'ACTIVE', quantity: 2000 }, + { holdingType: 'CLOSED', quantity: 0 } + ])( + 'should return $holdingType holdings when the holding type is specified', + async ({ holdingType, quantity }) => { + setUpCashOnlyPortfolio({ quantity }); + + const { holdings } = await portfolioService.getDetails({ + filters: [{ id: holdingType, type: 'HOLDING_TYPE' }], + userId: userDummyData.id + }); + + expect(holdings).toHaveLength(1); + expect(holdings[0].quantity).toBe(quantity); + } + ); + + it('should remove the holding type only from the snapshot filters', async () => { + setUpCashOnlyPortfolio({ quantity: 0 }); + + await portfolioService.getDetails({ + filters: [ + { id: AssetClass.EQUITY, type: 'ASSET_CLASS' }, + { id: 'CLOSED', type: 'HOLDING_TYPE' } + ], + userId: userDummyData.id + }); + + expect(portfolioCalculatorFactory.createCalculator).toHaveBeenCalledWith( + expect.objectContaining({ + filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }] + }) + ); + expect( + activitiesService.getActivitiesForPortfolioCalculator + ).toHaveBeenCalledWith({ + filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }], + userCurrency: 'CHF', + userId: userDummyData.id + }); + }); + }); + + describe('getHoldings', () => { + const activeHolding = { + assetProfile: { + isin: 'US0378331005', + name: 'Apple', + symbol: 'AAPL' + }, + quantity: 1 + }; + + const closedHolding = { + assetProfile: { + isin: 'US5949181045', + name: 'Microsoft', + symbol: 'MSFT' + }, + quantity: 0 + }; + + beforeEach(() => { + jest.spyOn(portfolioService, 'getDetails').mockResolvedValue({ + holdings: [activeHolding, closedHolding] + } as unknown as Awaited>); + }); + + it('should request all holdings when the holding type is not specified', async () => { + const holdings = await portfolioService.getHoldings({ + dateRange: 'max', + userId: userDummyData.id + }); + + expect(holdings).toEqual([activeHolding, closedHolding]); + expect(portfolioService.getDetails).toHaveBeenCalledWith({ + dateRange: 'max', + filters: undefined, + includeAllHoldings: true, + userId: userDummyData.id + }); + }); + + it('should find a closed holding when the holding type is not specified', async () => { + const holdings = await portfolioService.getHoldings({ + dateRange: 'max', + filters: [{ id: 'Microsoft', type: 'SEARCH_QUERY' }], + userId: userDummyData.id + }); + + expect(holdings).toEqual([closedHolding]); + }); + + it.each(['ACTIVE', 'CLOSED'])( + 'should not request all holdings when the holding type is %s', + async (holdingType) => { + await portfolioService.getHoldings({ + dateRange: 'max', + filters: [{ id: holdingType, type: 'HOLDING_TYPE' }], + userId: userDummyData.id + }); + + expect(portfolioService.getDetails).toHaveBeenCalledWith({ + dateRange: 'max', + filters: [{ id: holdingType, type: 'HOLDING_TYPE' }], + includeAllHoldings: false, + userId: userDummyData.id + }); + } + ); }); describe('getHolding', () => { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 19c2dd7948..60dab50329 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -477,12 +477,12 @@ export class PortfolioService { filters?: Filter[]; userId: string; }) { - const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy( - filters, - ({ type }) => { - return type; - } - ); + const { + HOLDING_TYPE: [filterByHoldingType] = [], + SEARCH_QUERY: [filterBySearchQuery] = [] + } = groupBy(filters, ({ type }) => { + return type; + }); const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => { return type !== 'SEARCH_QUERY'; @@ -491,7 +491,8 @@ export class PortfolioService { let { holdings } = await this.getDetails({ dateRange, userId, - filters: filtersWithoutSearchQueryFilter + filters: filtersWithoutSearchQueryFilter, + includeAllHoldings: !filterByHoldingType }); if (filterBySearchQuery) { @@ -592,6 +593,7 @@ export class PortfolioService { public async getDetails({ dateRange = DEFAULT_DATE_RANGE, filters, + includeAllHoldings = false, user: userFromCaller, userId, withExcludedAccounts = false, @@ -600,6 +602,7 @@ export class PortfolioService { }: { dateRange?: DateRange; filters?: Filter[]; + includeAllHoldings?: boolean; user?: UserWithSettings; userId: string; withExcludedAccounts?: boolean; @@ -614,19 +617,23 @@ export class PortfolioService { (user.settings?.settings as UserSettings)?.emergencyFund ?? 0 ); + const portfolioSnapshotFilters = filters?.filter(({ type }) => { + return type !== 'HOLDING_TYPE'; + }); + const { activities } = await this.activitiesService.getActivitiesForPortfolioCalculator({ - filters, userCurrency, - userId + userId, + filters: portfolioSnapshotFilters }); const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, - filters, userId, calculationType: this.getUserPerformanceCalculationType(user), - currency: userCurrency + currency: userCurrency, + filters: portfolioSnapshotFilters }); const { createdAt, currentValueInBaseCurrency, hasErrors, positions } = @@ -706,13 +713,13 @@ export class PortfolioService { tags, valueInBaseCurrency } of positions) { - if (isFilteredByClosedHoldings === true) { - if (!quantity.eq(0)) { + if (!includeAllHoldings) { + if (isFilteredByClosedHoldings && !quantity.eq(0)) { // Ignore positions with a quantity continue; } - } else { - if (quantity.eq(0)) { + + if (!isFilteredByClosedHoldings && quantity.eq(0)) { // Ignore positions without any quantity continue; } diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 518458e8a4..75186b1744 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -393,8 +393,12 @@ export class GfAccountDetailDialogComponent implements OnInit { .fetchPortfolioHoldings({ filters: [ { - type: 'ACCOUNT', - id: this.data.accountId + id: this.data.accountId, + type: 'ACCOUNT' + }, + { + id: 'ACTIVE', + type: 'HOLDING_TYPE' } ] }) diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 7f02cfb64e..f079e1c9c6 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -153,9 +153,7 @@ export class GfHomeHoldingsComponent implements OnInit { private fetchHoldings() { const filters = this.userService.getFilters(); - if (this.holdingType === 'CLOSED') { - filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' }); - } + filters.push({ id: this.holdingType, type: 'HOLDING_TYPE' }); return this.dataService.fetchPortfolioHoldings({ filters, diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 573d4e5bb6..63e7872c1e 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -367,7 +367,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { private loadHoldings() { this.dataService - .fetchPortfolioHoldings() + .fetchPortfolioHoldings({ + filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }] + }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.holdings = getHoldingsForFilter(holdings); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 853787f804..3b7938c8c4 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -153,7 +153,9 @@ export class GfCreateOrUpdateActivityDialogComponent { this.defaultDateFormat = getDateFormatString(this.locale); this.dataService - .fetchPortfolioHoldings() + .fetchPortfolioHoldings({ + filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }] + }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.defaultLookupItems = holdings 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 48f1d5abd0..98e0c0ad82 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 @@ -144,6 +144,10 @@ export class GfImportActivitiesDialogComponent { { id: AssetClass.FIXED_INCOME, type: 'ASSET_CLASS' + }, + { + id: 'ACTIVE', + type: 'HOLDING_TYPE' } ], range: DEFAULT_DATE_RANGE diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 3a3c44269d..26fb711bb7 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -377,7 +377,10 @@ export class GfAnalysisPageComponent implements OnInit { this.dataService .fetchPortfolioHoldings({ - filters: this.userService.getFilters(), + filters: [ + ...this.userService.getFilters(), + { id: 'ACTIVE', type: 'HOLDING_TYPE' } + ], range: this.user?.settings?.dateRange }) .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index bdbaa6cdb5..6babc10398 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -479,7 +479,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { this.setIsOpen(true); this.dataService - .fetchPortfolioHoldings() + .fetchPortfolioHoldings({ + filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }] + }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.holdings = getHoldingsForFilter(holdings); From eee5d08084a90282a9bf5dcf07591a92e6e8d679 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:45:11 +0200 Subject: [PATCH 4/8] Task/upgrade bull-board to version 9.5.0 (#7844) * Update bull-board to version 9.5.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 42 +++++++++++++++++++++--------------------- package.json | 6 +++--- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd97e5e31c..f363f4cc5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the loading state of the activity count in the portfolio summary - Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types +- Upgraded `bull-board` from version `9.0.1` to `9.5.0` - Upgraded `zod` from version `4.4.3` to `4.5.4` ## 3.68.0 - 2026-09-06 diff --git a/package-lock.json b/package-lock.json index 563f7d4e2d..ba53c3025b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,9 @@ "@angular/platform-browser-dynamic": "22.1.2", "@angular/router": "22.1.2", "@angular/service-worker": "22.1.2", - "@bull-board/api": "9.0.1", - "@bull-board/express": "9.0.1", - "@bull-board/nestjs": "9.0.1", + "@bull-board/api": "9.5.0", + "@bull-board/express": "9.5.0", + "@bull-board/nestjs": "9.5.0", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.7", @@ -6166,17 +6166,17 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bull-board/api": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.0.1.tgz", - "integrity": "sha512-QZ+SDOlD1MAePNqy8qfs+7zExZyJCW/+nEncGE6Mt/+MlxT9mqvyZWmPUm38QZvIBJGSKaA9i9ZElR9wDbbPhw==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.5.0.tgz", + "integrity": "sha512-fmOyOy1lZiLZFyDJjI2osZrpTMO+JSgmGNhDf3MrmOf3oF084o6w0wpGefs0OqEfyUT1F5/1arE5AwmEUuxZ+A==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "9.0.1", + "@bull-board/ui": "9.5.0", "bull": "^4.16.5", - "bullmq": "^5.79.2 || ^6.0.0" + "bullmq": "^5.56.0 || ^6.0.0" }, "peerDependenciesMeta": { "bull": { @@ -6188,13 +6188,13 @@ } }, "node_modules/@bull-board/express": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.0.1.tgz", - "integrity": "sha512-eblNP2imr639K9KG0n1sBaJp6lw6pu4/j7q3W+bydxtw83v4wobCcuVUxuhQkRaLTrIUMR/Snulc7+/vtxCshw==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.5.0.tgz", + "integrity": "sha512-EKeXQzeb1zF9ra0hfYfPArU1JlWKl2vyL4Rb3gQW0XzU7SkEFgYm3727gDrQZdU8dvc5g/FLEqc0dr6UkDUHhg==", "license": "MIT", "dependencies": { - "@bull-board/api": "9.0.1", - "@bull-board/ui": "9.0.1", + "@bull-board/api": "9.5.0", + "@bull-board/ui": "9.5.0", "ejs": "^6.0.1", "express": "^5.2.1" } @@ -6212,12 +6212,12 @@ } }, "node_modules/@bull-board/nestjs": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.0.1.tgz", - "integrity": "sha512-pVcMZyuZ/X8SLjoayW8aREM1f4RFdI9WlwvHmT9RwlPhRnFLTnGMbnC5LOk+cYR/9WTB/Oeo1SDnzblIR99JtQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.5.0.tgz", + "integrity": "sha512-0P/Ru3cbOnf6GwnDGv4iPJ/NEjbcaicdpYMISpp6K7JZ24RVkCnmoUIKt8KbQdjZGqfH2EWmyIRC2WhiUE3T+g==", "license": "MIT", "peerDependencies": { - "@bull-board/api": "^9.0.1", + "@bull-board/api": "^9.5.0", "@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", @@ -6226,12 +6226,12 @@ } }, "node_modules/@bull-board/ui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.0.1.tgz", - "integrity": "sha512-yGg0CMAFCA49IM1zPS+JFvUJW/whftaLtRyMWiVtvyLRd53uX0QW/uTaPcHy0V4bApp6U0FTa9+L9rk/49ciwg==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.5.0.tgz", + "integrity": "sha512-qrq0e906M8Ps/mV3bQXmj7EXJWLaFs5mNz+oBoLR/7EShBi5YooROzQ/SEFFedg/L9h6QWDWJ9Ph0AH7NrG9uw==", "license": "MIT", "dependencies": { - "@bull-board/api": "9.0.1" + "@bull-board/api": "9.5.0" } }, "node_modules/@cacheable/utils": { diff --git a/package.json b/package.json index 1ddbf7ea64..1f13ab86c2 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "@angular/platform-browser-dynamic": "22.1.2", "@angular/router": "22.1.2", "@angular/service-worker": "22.1.2", - "@bull-board/api": "9.0.1", - "@bull-board/express": "9.0.1", - "@bull-board/nestjs": "9.0.1", + "@bull-board/api": "9.5.0", + "@bull-board/express": "9.5.0", + "@bull-board/nestjs": "9.5.0", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.7", From ceaef13b70d5293c90e6d18518f45b3363e85135 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:58:52 +0200 Subject: [PATCH 5/8] Task/migrate watchlist to dedicated routes (#7837) * Migrate create dialog to dedicated route * Update changelog --- CHANGELOG.md | 1 + .../home-watchlist.component.ts | 66 ++-------- .../home-watchlist/home-watchlist.html | 8 +- .../home-watchlist/home-watchlist.service.ts | 15 +++ .../watchlist-dialog-host.component.ts | 114 ++++++++++++++++++ .../src/app/pages/home/home-page.routes.ts | 9 ++ libs/common/src/lib/routes/routes.ts | 7 ++ 7 files changed, 162 insertions(+), 58 deletions(-) create mode 100644 apps/client/src/app/components/home-watchlist/home-watchlist.service.ts create mode 100644 apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f363f4cc5f..9bac4d2e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the loading state of the activity count in the portfolio summary +- Migrated the create dialog of the watchlist to a dedicated route - Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types - Upgraded `bull-board` from version `9.0.1` to `9.5.0` - Upgraded `zod` from version `4.4.3` to `4.5.4` diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts index d3a7fb203f..b56d8d66ea 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts @@ -6,6 +6,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfFabComponent } from '@ghostfolio/ui/fab'; @@ -23,12 +24,10 @@ import { OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatDialog } from '@angular/material/dialog'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterModule } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { GfCreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component'; -import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces'; +import { HomeWatchlistService } from './home-watchlist.service'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -38,6 +37,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/ GfPremiumIndicatorComponent, RouterModule ], + providers: [HomeWatchlistService], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-home-watchlist', styleUrls: ['./home-watchlist.scss'], @@ -45,6 +45,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/ }) export class GfHomeWatchlistComponent implements OnInit { protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE; + protected readonly internalRoutes = internalRoutes; protected hasPermissionToCreateWatchlistItem: boolean; protected hasPermissionToDeleteWatchlistItem: boolean; @@ -59,18 +60,14 @@ export class GfHomeWatchlistComponent implements OnInit { private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); - private readonly dialog = inject(MatDialog); - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); + private readonly homeWatchlistService = inject(HomeWatchlistService); private readonly userService = inject(UserService); public constructor() { - this.route.queryParams + this.homeWatchlistService.refresh$ .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((params) => { - if (params['createWatchlistItemDialog']) { - this.openCreateWatchlistItemDialog(); - } + .subscribe(() => { + this.loadWatchlistData(); }); this.userService.stateChanged @@ -130,49 +127,4 @@ export class GfHomeWatchlistComponent implements OnInit { } }); } - - private openCreateWatchlistItemDialog() { - this.userService - .get() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((user) => { - this.user = user; - - if ( - !hasPermission(user?.permissions, permissions.createWatchlistItem) || - !hasScope(user?.scopes, scopes.watchlistCreate) - ) { - this.router.navigate(['.'], { relativeTo: this.route }); - - return; - } - - const dialogRef = this.dialog.open< - GfCreateWatchlistItemDialogComponent, - CreateWatchlistItemDialogParams - >(GfCreateWatchlistItemDialogComponent, { - data: { - deviceType: this.deviceType(), - locale: this.user?.settings?.locale ?? DEFAULT_LOCALE - }, - width: this.deviceType() === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ dataSource, symbol } = {}) => { - if (dataSource && symbol) { - this.dataService - .postWatchlistItem({ dataSource, symbol }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: () => this.loadWatchlistData() - }); - } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - }); - } } diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html index fdd894de4e..11b2d9c2fe 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.html +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html @@ -22,5 +22,11 @@
@if (hasPermissionToCreateWatchlistItem) { - + } + + diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts new file mode 100644 index 0000000000..47eafe6059 --- /dev/null +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts @@ -0,0 +1,15 @@ +import { Service } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Service({ autoProvided: false }) +export class HomeWatchlistService { + private readonly refreshSubject = new Subject(); + + public get refresh$() { + return this.refreshSubject.asObservable(); + } + + public triggerRefresh() { + this.refreshSubject.next(); + } +} diff --git a/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts b/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts new file mode 100644 index 0000000000..9e1887e278 --- /dev/null +++ b/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts @@ -0,0 +1,114 @@ +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; +import { User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { hasScope, scopes } from '@ghostfolio/common/scopes'; +import { DataService } from '@ghostfolio/ui/services'; + +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + OnDestroy, + OnInit +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { DeviceDetectorService } from 'ngx-device-detector'; + +import { GfCreateWatchlistItemDialogComponent } from '../create-watchlist-item-dialog/create-watchlist-item-dialog.component'; +import { CreateWatchlistItemDialogParams } from '../create-watchlist-item-dialog/interfaces/interfaces'; +import { HomeWatchlistService } from '../home-watchlist.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-watchlist-dialog-host', + template: '' +}) +export class GfWatchlistDialogHostComponent implements OnDestroy, OnInit { + private dialogRef: MatDialogRef; + + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly homeWatchlistService = inject(HomeWatchlistService); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public ngOnInit() { + this.userService + .get() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((user) => { + if ( + !hasPermission(user?.permissions, permissions.createWatchlistItem) || + !hasScope(user?.scopes, scopes.watchlistCreate) + ) { + this.navigateBack(); + + return; + } + + this.openDialog(user); + }); + } + + public ngOnDestroy() { + // The dialog lives in an overlay outside of this component, so it needs to + // be closed explicitly when leaving the route (for example via the browser + // navigation) + this.dialogRef?.close(); + } + + private navigateBack() { + void this.router.navigate( + internalRoutes.home.subRoutes.watchlist.routerLink + ); + } + + private openDialog(user: User) { + const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; + + const dialogRef = this.dialog.open< + GfCreateWatchlistItemDialogComponent, + CreateWatchlistItemDialogParams + >(GfCreateWatchlistItemDialogComponent, { + data: { + deviceType, + locale: user?.settings?.locale ?? DEFAULT_LOCALE + }, + width: deviceType === 'mobile' ? '100vw' : '50rem' + }); + + this.dialogRef = dialogRef; + + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ dataSource, symbol } = {}) => { + if (!dataSource || !symbol) { + this.navigateBack(); + + return; + } + + this.dataService + .postWatchlistItem({ dataSource, symbol }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + error: () => { + this.navigateBack(); + }, + next: () => { + this.homeWatchlistService.triggerRefresh(); + + this.navigateBack(); + } + }); + }); + } +} diff --git a/apps/client/src/app/pages/home/home-page.routes.ts b/apps/client/src/app/pages/home/home-page.routes.ts index cc444c2b00..3e0ce7b266 100644 --- a/apps/client/src/app/pages/home/home-page.routes.ts +++ b/apps/client/src/app/pages/home/home-page.routes.ts @@ -2,6 +2,7 @@ import { GfHomeHoldingsComponent } from '@ghostfolio/client/components/home-hold import { GfHomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component'; import { GfHomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component'; import { GfHomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component'; +import { GfWatchlistDialogHostComponent } from '@ghostfolio/client/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component'; import { GfMarketsComponent } from '@ghostfolio/client/components/markets/markets.component'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; @@ -34,6 +35,14 @@ export const routes: Routes = [ title: internalRoutes.home.subRoutes.markets.title }, { + children: [ + { + component: GfWatchlistDialogHostComponent, + path: internalRoutes.home.subRoutes.watchlist.subRoutes.create.path, + title: + internalRoutes.home.subRoutes.watchlist.subRoutes.create.title + } + ], path: internalRoutes.home.subRoutes.watchlist.path, component: GfHomeWatchlistComponent, title: internalRoutes.home.subRoutes.watchlist.title diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index b45715607a..ca3cdd3663 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -156,6 +156,13 @@ export const internalRoutes = { watchlist: { path: 'watchlist', routerLink: ['/home', 'watchlist'], + subRoutes: { + create: { + path: 'create', + routerLink: ['/home', 'watchlist', 'create'], + title: $localize`Add to Watchlist` + } + }, title: $localize`Watchlist` } }, From e43e2178db4e969c42f1f9024d076ab8e0e1ed70 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:12:09 +0200 Subject: [PATCH 6/8] Task/upgrade countries-and-timezones to version 3.10.0 (#7845) * Update countries-and-timezones to version 3.10.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bac4d2e12..5bdc636c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the loading state of the activity count in the portfolio summary - Migrated the create dialog of the watchlist to a dedicated route - Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types +- Upgraded `countries-and-timezones` from version `3.9.0` to `3.10.0` - Upgraded `bull-board` from version `9.0.1` to `9.5.0` - Upgraded `zod` from version `4.4.3` to `4.5.4` diff --git a/package-lock.json b/package-lock.json index ba53c3025b..c740e90032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "class-validator": "0.15.1", "color": "5.0.3", "cookie-parser": "1.4.7", - "countries-and-timezones": "3.9.0", + "countries-and-timezones": "3.10.0", "countries-list": "3.4.1", "countup.js": "2.10.1", "date-fns": "4.4.0", @@ -19044,9 +19044,9 @@ } }, "node_modules/countries-and-timezones": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.9.0.tgz", - "integrity": "sha512-3upkVtUfZgdp5xOQI/Deg1ye73ae6bygJMO4UvtUi6wLjwcj7cBqx2PzSZ1VL8i5iveBbjhsQp2Judx7kPKivA==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.10.0.tgz", + "integrity": "sha512-HQ1GtXWNZy0r5MnZMLF9xmBmFNmtDVBeKQgX/5DY+RVmcZTODA+EPN/JLPCYRNP86nHG0QQrb1yJwGzXeqCCXw==", "license": "MIT", "engines": { "node": ">=8.x", diff --git a/package.json b/package.json index 1f13ab86c2..7d8e6edca8 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "class-validator": "0.15.1", "color": "5.0.3", "cookie-parser": "1.4.7", - "countries-and-timezones": "3.9.0", + "countries-and-timezones": "3.10.0", "countries-list": "3.4.1", "countup.js": "2.10.1", "date-fns": "4.4.0", From 0c2ebee8ac7f9551e7c54dd9199655968beb36bf Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:12:30 +0200 Subject: [PATCH 7/8] Bugfix/opening asset profile from watchlist (#7846) * Fix issue when opening asset profile * Update changelog --- CHANGELOG.md | 4 ++++ .../benchmark-detail-dialog.component.ts | 2 +- .../benchmark-detail-dialog/benchmark-detail-dialog.html | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bdc636c9b..af0c7bfffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `bull-board` from version `9.0.1` to `9.5.0` - Upgraded `zod` from version `4.4.3` to `4.5.4` +### Fixed + +- Resolved an issue when opening an asset profile from the watchlist + ## 3.68.0 - 2026-09-06 ### Added diff --git a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts index 8a7e131568..f46722921f 100644 --- a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts +++ b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts @@ -44,7 +44,7 @@ import { BenchmarkDetailDialogParams } from './interfaces/interfaces'; templateUrl: 'benchmark-detail-dialog.html' }) export class GfBenchmarkDetailDialogComponent implements OnInit { - public assetProfile: AdminMarketDataDetails['assetProfile']; + public assetProfile?: AdminMarketDataDetails['assetProfile']; public historicalDataItems: LineChartItem[]; public isLoading = true; public value: number; diff --git a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html index 2f415d874c..2ab60b9d2d 100644 --- a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html +++ b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html @@ -1,7 +1,7 @@ From fb8691e16a23c8b222b727947635c1194f56a657 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:15:06 +0200 Subject: [PATCH 8/8] Release 3.69.0 (#7847) --- 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 af0c7bfffd..8bbc07ae2d 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.69.0 - 2026-09-07 ### Changed diff --git a/package-lock.json b/package-lock.json index c740e90032..8a2818c967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.68.0", + "version": "3.69.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.68.0", + "version": "3.69.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 7d8e6edca8..517bca30cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.68.0", + "version": "3.69.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",