Browse Source

Merge branch 'main' into task/improve-style-of-tabs-in-various-dialogs-on-mobile

pull/7481/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
1896b7e952
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      CHANGELOG.md
  2. 7
      apps/api/src/app/admin/admin.service.ts
  3. 4
      apps/api/src/app/health/health.service.ts
  4. 208
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 4
      apps/api/src/app/subscription/subscription.controller.ts
  6. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  7. 70
      apps/api/src/services/property/property.service.ts
  8. 4
      apps/client/src/locales/messages.de.xlf
  9. 4
      libs/ui/src/lib/activities-table/activities-table.component.html
  10. 9
      libs/ui/src/lib/activities-table/activities-table.component.scss
  11. 25
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  12. 7
      libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html

18
CHANGELOG.md

@ -12,6 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the tabs in the account detail dialog on mobile
- Improved the style of the tabs in the holding detail dialog on mobile
- Improved the style of the tabs in the asset profile dialog of the admin control panel on mobile
- 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`)
### 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

7
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<string[]>(PROPERTY_CURRENCIES);
const customCurrencies = await this.propertyService.getByKey<string[]>(
PROPERTY_CURRENCIES,
{ skipCache: true }
);
if (customCurrencies.includes(currency)) {
const updatedCustomCurrencies = customCurrencies.filter(

4
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 {

208
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',

4
apps/api/src/app/subscription/subscription.controller.ts

@ -54,7 +54,9 @@ export class SubscriptionController {
}
let coupons =
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS)) ?? [];
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS, {
skipCache: true
})) ?? [];
const coupon = coupons.find((currentCoupon) => {
return currentCoupon.code === couponCode;

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -159,7 +159,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS
PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id });
@ -196,7 +197,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS
PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {

70
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<Property[]>;
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<TValue extends PropertyValue>(aKey: PropertyKey) {
const properties = await this.get();
public async getByKey<TValue extends PropertyValue>(
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;
}
}

4
apps/client/src/locales/messages.de.xlf

@ -351,7 +351,7 @@
</trans-unit>
<trans-unit id="4467323362722952678" datatype="html">
<source>Unknown</source>
<target state="new">Unknown</target>
<target state="translated">Unbekannt</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">88</context>
@ -1767,7 +1767,7 @@
</trans-unit>
<trans-unit id="659291745893337969" datatype="html">
<source>Invalid API key</source>
<target state="new">Invalid API key</target>
<target state="translated">Ungültiger API-Schlüssel</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context>
<context context-type="linenumber">112</context>

4
libs/ui/src/lib/activities-table/activities-table.component.html

@ -1,4 +1,6 @@
<div class="d-flex justify-content-end justify-content-lg-between">
<div
class="align-items-center d-flex justify-content-end justify-content-lg-between"
>
<div class="d-none d-lg-flex">
@if (hasPermissionToFilterByType) {
<mat-form-field appearance="outline" class="without-hint">

9
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
)
);
}

25
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
}
};

7
libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html

@ -12,10 +12,11 @@
<div>
<div class="h4 mb-0">
<span class="mr-2">{{ fearAndGreedIndexText() }}</span>
@let value = fearAndGreedIndex() | number: '1.0-0';
<small class="text-muted"
><strong>{{
(fearAndGreedIndex() | number: '1.0-0') ?? placeholder
}}</strong
><span [class.font-weight-bold]="value">{{
value ?? placeholder
}}</span
>/100</small
>
</div>

Loading…
Cancel
Save