@if (hasPermissionToFilterByType) {
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.scss b/libs/ui/src/lib/activities-table/activities-table.component.scss
index 5d4e87f30f..4e98f726e2 100644
--- a/libs/ui/src/lib/activities-table/activities-table.component.scss
+++ b/libs/ui/src/lib/activities-table/activities-table.component.scss
@@ -1,3 +1,12 @@
+@use '@angular/material' as mat;
+
:host {
display: block;
+
+ @include mat.form-field-overrides(
+ (
+ container-height: 2rem,
+ container-vertical-padding: 0.33rem
+ )
+ );
}
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 57a8f18b68..057747cf14 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
@@ -494,3 +494,28 @@ export const Actions: Story = {
totalItems: activities.length
}
};
+
+export const Toolbar: Story = {
+ args: {
+ dataSource,
+ baseCurrency: 'USD',
+ deviceType: 'desktop',
+ hasActivities: true,
+ hasPermissionToCreateActivity: true,
+ hasPermissionToDeleteActivity: true,
+ hasPermissionToExportActivities: true,
+ hasPermissionToFilterByType: true,
+ hasPermissionToOpenDetails: false,
+ locale: 'en-US',
+ pageIndex: 0,
+ pageSize: 10,
+ showAccountColumn: true,
+ showActions: false,
+ showCheckbox: false,
+ showNameColumn: true,
+ sortColumn: 'date',
+ sortDirection: 'desc',
+ sortDisabled: false,
+ totalItems: activities.length
+ }
+};
From df2e96fc6f8447c6e0dc9081e01ee8edb44dba67 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 31 Jul 2026 18:20:57 +0200
Subject: [PATCH 4/5] Task/improve performance of property service by caching
properties (#7484)
* Improve performance by caching properties in memory
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/admin/admin.service.ts | 7 +-
apps/api/src/app/health/health.service.ts | 4 +-
.../subscription/subscription.controller.ts | 4 +-
.../services/benchmark/benchmark.service.ts | 6 +-
.../src/services/property/property.service.ts | 70 +++++++++++++++++--
6 files changed, 80 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 477048f594..d5c6481e3a 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 style of the empty state in the _Fear & Greed Index_ component
- Improved the style of the type filter in the activities table component (experimental)
+- Improved the performance of the property service by caching the properties in memory
- Improved the language localization for German (`de`)
## 3.37.0 - 2026-07-30
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index 26a4e06f47..4c608e0fd2 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -107,8 +107,11 @@ export class AdminService {
await this.marketDataService.deleteMany({ dataSource, symbol });
const currency = getCurrencyFromSymbol(symbol);
- const customCurrencies =
- await this.propertyService.getByKey(PROPERTY_CURRENCIES);
+
+ const customCurrencies = await this.propertyService.getByKey(
+ PROPERTY_CURRENCIES,
+ { skipCache: true }
+ );
if (customCurrencies.includes(currency)) {
const updatedCustomCurrencies = customCurrencies.filter(
diff --git a/apps/api/src/app/health/health.service.ts b/apps/api/src/app/health/health.service.ts
index f08f33a1e3..42a0be61b8 100644
--- a/apps/api/src/app/health/health.service.ts
+++ b/apps/api/src/app/health/health.service.ts
@@ -26,7 +26,9 @@ export class HealthService {
public async isDatabaseHealthy() {
try {
- await this.propertyService.getByKey(PROPERTY_CURRENCIES);
+ await this.propertyService.getByKey(PROPERTY_CURRENCIES, {
+ skipCache: true
+ });
return true;
} catch {
diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts
index 4018e4753e..a70fe87916 100644
--- a/apps/api/src/app/subscription/subscription.controller.ts
+++ b/apps/api/src/app/subscription/subscription.controller.ts
@@ -54,7 +54,9 @@ export class SubscriptionController {
}
let coupons =
- (await this.propertyService.getByKey(PROPERTY_COUPONS)) ?? [];
+ (await this.propertyService.getByKey(PROPERTY_COUPONS, {
+ skipCache: true
+ })) ?? [];
const coupon = coupons.find((currentCoupon) => {
return currentCoupon.code === couponCode;
diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts
index 17e729f9f4..993e0f0aae 100644
--- a/apps/api/src/services/benchmark/benchmark.service.ts
+++ b/apps/api/src/services/benchmark/benchmark.service.ts
@@ -159,7 +159,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey(
- PROPERTY_BENCHMARKS
+ PROPERTY_BENCHMARKS,
+ { skipCache: true }
)) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id });
@@ -196,7 +197,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey(
- PROPERTY_BENCHMARKS
+ PROPERTY_BENCHMARKS,
+ { skipCache: true }
)) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {
diff --git a/apps/api/src/services/property/property.service.ts b/apps/api/src/services/property/property.service.ts
index 80643482fa..6d8130bbc2 100644
--- a/apps/api/src/services/property/property.service.ts
+++ b/apps/api/src/services/property/property.service.ts
@@ -6,27 +6,39 @@ import {
import { PropertyKey } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
+import { Property } from '@prisma/client';
+import { addMilliseconds, isBefore } from 'date-fns';
+import ms from 'ms';
import { PropertyValue } from './interfaces/interfaces';
@Injectable()
export class PropertyService {
+ private static readonly CACHE_TTL = ms('1 minute');
+
+ private cachedProperties: Promise;
+ private cachedPropertiesExpiresAt: Date;
+
public constructor(private readonly prismaService: PrismaService) {}
public async delete({ key }: { key: PropertyKey }) {
- return this.prismaService.property.delete({
+ const property = await this.prismaService.property.delete({
where: { key }
});
+
+ this.invalidateCache();
+
+ return property;
}
- public async get() {
+ public async get({ skipCache = false } = {}) {
const response: {
[key: string]: PropertyValue;
} = {
[PROPERTY_CURRENCIES]: []
};
- const properties = await this.prismaService.property.findMany();
+ const properties = await this.getProperties({ skipCache });
for (const property of properties) {
let value = property.value;
@@ -41,8 +53,11 @@ export class PropertyService {
return response;
}
- public async getByKey(aKey: PropertyKey) {
- const properties = await this.get();
+ public async getByKey(
+ aKey: PropertyKey,
+ { skipCache = false } = {}
+ ) {
+ const properties = await this.get({ skipCache });
return properties[aKey] as TValue;
}
@@ -53,10 +68,53 @@ export class PropertyService {
}
public async put({ key, value }: { key: PropertyKey; value: string }) {
- return this.prismaService.property.upsert({
+ const property = await this.prismaService.property.upsert({
create: { key, value },
update: { value },
where: { key }
});
+
+ this.invalidateCache();
+
+ return property;
+ }
+
+ /**
+ * Returns the properties from the in-memory cache, falling back to the
+ * database. Callers which write back a modified property must set
+ * skipCache to avoid basing the write on a stale read.
+ */
+ private async getProperties({ skipCache = false } = {}) {
+ if (skipCache) {
+ return this.prismaService.property.findMany();
+ }
+
+ if (
+ this.cachedProperties &&
+ isBefore(new Date(), this.cachedPropertiesExpiresAt)
+ ) {
+ return this.cachedProperties;
+ }
+
+ const properties = this.prismaService.property.findMany().catch((error) => {
+ if (this.cachedProperties === properties) {
+ this.invalidateCache();
+ }
+
+ throw error;
+ });
+
+ this.cachedProperties = properties;
+ this.cachedPropertiesExpiresAt = addMilliseconds(
+ new Date(),
+ PropertyService.CACHE_TTL
+ );
+
+ return this.cachedProperties;
+ }
+
+ private invalidateCache() {
+ this.cachedProperties = undefined;
+ this.cachedPropertiesExpiresAt = undefined;
}
}
From 1770350516c05dddf817f6d727dcdb409f8a873f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 31 Jul 2026 18:21:48 +0200
Subject: [PATCH 5/5] Bugfix/static portfolio analysis rules for portfolio with
no holdings (#7466)
* Fix static portfolio analysis rules for portfolio with no holdings
* Update changelog
---
CHANGELOG.md | 14 ++
.../src/app/portfolio/portfolio.service.ts | 208 +++++++++---------
2 files changed, 117 insertions(+), 105 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c6481e3a..d14f77f5e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the performance of the property service by caching the properties in memory
- Improved the language localization for German (`de`)
+### Fixed
+
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Equity)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Fixed Income)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment: Base Currency)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Developed Markets)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Emerging Markets)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Asia-Pacific)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Emerging Markets)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Europe)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Japan)
+- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (North America)
+
## 3.37.0 - 2026-07-30
### Added
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 6a3c1f145a..d1aff38111 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -1126,6 +1126,8 @@ export class PortfolioService {
withSummary: true
});
+ const hasOpenHoldings = Object.keys(holdings).length > 0;
+
const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency);
@@ -1185,26 +1187,25 @@ export class PortfolioService {
id: 'rule.currencyClusterRisk.category',
languageCode: userSettings.language
}),
- rules:
- summary.activityCount > 0
- ? await this.rulesService.evaluate(
- [
- new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
- this.exchangeRateDataService,
- this.i18nService,
- Object.values(holdings),
- userSettings.language
- ),
- new CurrencyClusterRiskCurrentInvestment(
- this.exchangeRateDataService,
- this.i18nService,
- Object.values(holdings),
- userSettings.language
- )
- ],
- userSettings
- )
- : undefined
+ rules: hasOpenHoldings
+ ? await this.rulesService.evaluate(
+ [
+ new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
+ this.exchangeRateDataService,
+ this.i18nService,
+ Object.values(holdings),
+ userSettings.language
+ ),
+ new CurrencyClusterRiskCurrentInvestment(
+ this.exchangeRateDataService,
+ this.i18nService,
+ Object.values(holdings),
+ userSettings.language
+ )
+ ],
+ userSettings
+ )
+ : undefined
},
{
key: 'assetClassClusterRisk',
@@ -1212,26 +1213,25 @@ export class PortfolioService {
id: 'rule.assetClassClusterRisk.category',
languageCode: userSettings.language
}),
- rules:
- summary.activityCount > 0
- ? await this.rulesService.evaluate(
- [
- new AssetClassClusterRiskEquity(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- Object.values(holdings)
- ),
- new AssetClassClusterRiskFixedIncome(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- Object.values(holdings)
- )
- ],
- userSettings
- )
- : undefined
+ rules: hasOpenHoldings
+ ? await this.rulesService.evaluate(
+ [
+ new AssetClassClusterRiskEquity(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ Object.values(holdings)
+ ),
+ new AssetClassClusterRiskFixedIncome(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ Object.values(holdings)
+ )
+ ],
+ userSettings
+ )
+ : undefined
},
{
key: 'accountClusterRisk',
@@ -1266,28 +1266,27 @@ export class PortfolioService {
id: 'rule.economicMarketClusterRisk.category',
languageCode: userSettings.language
}),
- rules:
- summary.activityCount > 0
- ? await this.rulesService.evaluate(
- [
- new EconomicMarketClusterRiskDevelopedMarkets(
- this.exchangeRateDataService,
- this.i18nService,
- marketsTotalInBaseCurrency,
- markets.developedMarkets.valueInBaseCurrency,
- userSettings.language
- ),
- new EconomicMarketClusterRiskEmergingMarkets(
- this.exchangeRateDataService,
- this.i18nService,
- marketsTotalInBaseCurrency,
- markets.emergingMarkets.valueInBaseCurrency,
- userSettings.language
- )
- ],
- userSettings
- )
- : undefined
+ rules: hasOpenHoldings
+ ? await this.rulesService.evaluate(
+ [
+ new EconomicMarketClusterRiskDevelopedMarkets(
+ this.exchangeRateDataService,
+ this.i18nService,
+ marketsTotalInBaseCurrency,
+ markets.developedMarkets.valueInBaseCurrency,
+ userSettings.language
+ ),
+ new EconomicMarketClusterRiskEmergingMarkets(
+ this.exchangeRateDataService,
+ this.i18nService,
+ marketsTotalInBaseCurrency,
+ markets.emergingMarkets.valueInBaseCurrency,
+ userSettings.language
+ )
+ ],
+ userSettings
+ )
+ : undefined
},
{
key: 'regionalMarketClusterRisk',
@@ -1295,49 +1294,48 @@ export class PortfolioService {
id: 'rule.regionalMarketClusterRisk.category',
languageCode: userSettings.language
}),
- rules:
- summary.activityCount > 0
- ? await this.rulesService.evaluate(
- [
- new RegionalMarketClusterRiskAsiaPacific(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- marketsAdvancedTotalInBaseCurrency,
- marketsAdvanced.asiaPacific.valueInBaseCurrency
- ),
- new RegionalMarketClusterRiskEmergingMarkets(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- marketsAdvancedTotalInBaseCurrency,
- marketsAdvanced.emergingMarkets.valueInBaseCurrency
- ),
- new RegionalMarketClusterRiskEurope(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- marketsAdvancedTotalInBaseCurrency,
- marketsAdvanced.europe.valueInBaseCurrency
- ),
- new RegionalMarketClusterRiskJapan(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- marketsAdvancedTotalInBaseCurrency,
- marketsAdvanced.japan.valueInBaseCurrency
- ),
- new RegionalMarketClusterRiskNorthAmerica(
- this.exchangeRateDataService,
- this.i18nService,
- userSettings.language,
- marketsAdvancedTotalInBaseCurrency,
- marketsAdvanced.northAmerica.valueInBaseCurrency
- )
- ],
- userSettings
- )
- : undefined
+ rules: hasOpenHoldings
+ ? await this.rulesService.evaluate(
+ [
+ new RegionalMarketClusterRiskAsiaPacific(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.asiaPacific.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskEmergingMarkets(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.emergingMarkets.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskEurope(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.europe.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskJapan(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.japan.valueInBaseCurrency
+ ),
+ new RegionalMarketClusterRiskNorthAmerica(
+ this.exchangeRateDataService,
+ this.i18nService,
+ userSettings.language,
+ marketsAdvancedTotalInBaseCurrency,
+ marketsAdvanced.northAmerica.valueInBaseCurrency
+ )
+ ],
+ userSettings
+ )
+ : undefined
},
{
key: 'fees',