diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts
index aa176c0f0..a45efd9b4 100644
--- a/apps/client/src/app/pages/api/api-page.component.ts
+++ b/apps/client/src/app/pages/api/api-page.component.ts
@@ -1,3 +1,7 @@
+import {
+ HEADER_KEY_SKIP_INTERCEPTOR,
+ HEADER_KEY_TOKEN
+} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioStatusResponse,
@@ -8,7 +12,7 @@ import {
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
-import { HttpClient, HttpParams } from '@angular/common/http';
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { format, startOfYear } from 'date-fns';
import { map, Observable, Subject, takeUntil } from 'rxjs';
@@ -28,11 +32,14 @@ export class GfApiPageComponent implements OnInit {
public status$: Observable
;
public symbols$: Observable;
+ private apiKey: string;
private unsubscribeSubject = new Subject();
public constructor(private http: HttpClient) {}
public ngOnInit() {
+ this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`);
+
this.dividends$ = this.fetchDividends({ symbol: 'KO' });
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL.US' });
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] });
@@ -52,8 +59,11 @@ export class GfApiPageComponent implements OnInit {
return this.http
.get(
- `/api/v1/data-providers/ghostfolio/dividends/${symbol}`,
- { params }
+ `/api/v2/data-providers/ghostfolio/dividends/${symbol}`,
+ {
+ params,
+ headers: this.getHeaders()
+ }
)
.pipe(
map(({ dividends }) => {
@@ -70,8 +80,11 @@ export class GfApiPageComponent implements OnInit {
return this.http
.get(
- `/api/v1/data-providers/ghostfolio/historical/${symbol}`,
- { params }
+ `/api/v2/data-providers/ghostfolio/historical/${symbol}`,
+ {
+ params,
+ headers: this.getHeaders()
+ }
)
.pipe(
map(({ historicalData }) => {
@@ -85,8 +98,9 @@ export class GfApiPageComponent implements OnInit {
const params = new HttpParams().set('symbols', symbols.join(','));
return this.http
- .get('/api/v1/data-providers/ghostfolio/quotes', {
- params
+ .get('/api/v2/data-providers/ghostfolio/quotes', {
+ params,
+ headers: this.getHeaders()
})
.pipe(
map(({ quotes }) => {
@@ -99,7 +113,8 @@ export class GfApiPageComponent implements OnInit {
private fetchStatus() {
return this.http
.get(
- '/api/v1/data-providers/ghostfolio/status'
+ '/api/v2/data-providers/ghostfolio/status',
+ { headers: this.getHeaders() }
)
.pipe(takeUntil(this.unsubscribeSubject));
}
@@ -118,8 +133,9 @@ export class GfApiPageComponent implements OnInit {
}
return this.http
- .get('/api/v1/data-providers/ghostfolio/lookup', {
- params
+ .get('/api/v2/data-providers/ghostfolio/lookup', {
+ params,
+ headers: this.getHeaders()
})
.pipe(
map(({ items }) => {
@@ -128,4 +144,11 @@ export class GfApiPageComponent implements OnInit {
takeUntil(this.unsubscribeSubject)
);
}
+
+ private getHeaders() {
+ return new HttpHeaders({
+ [HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
+ [HEADER_KEY_TOKEN]: `Api-Key ${this.apiKey}`
+ });
+ }
}
diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts
index 5d252f00f..77d135f57 100644
--- a/apps/client/src/app/services/admin.service.ts
+++ b/apps/client/src/app/services/admin.service.ts
@@ -24,7 +24,7 @@ import {
Filter
} from '@ghostfolio/common/interfaces';
-import { HttpClient, HttpParams } from '@angular/common/http';
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { DataSource, MarketData, Platform, Tag } from '@prisma/client';
@@ -147,14 +147,14 @@ export class AdminService {
public fetchGhostfolioDataProviderStatus() {
return this.fetchAdminData().pipe(
switchMap(({ settings }) => {
+ const headers = new HttpHeaders({
+ [HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
+ [HEADER_KEY_TOKEN]: `Api-Key ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}`
+ });
+
return this.http.get(
- `${environment.production ? 'https://ghostfol.io' : ''}/api/v1/data-providers/ghostfolio/status`,
- {
- headers: {
- [HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
- [HEADER_KEY_TOKEN]: `Bearer ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}`
- }
- }
+ `${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
+ { headers }
);
})
);
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index dccbb064a..92d030827 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -22,6 +22,7 @@ import {
AccountBalancesResponse,
Accounts,
AdminMarketDataDetails,
+ ApiKeyResponse,
AssetProfileIdentifier,
BenchmarkMarketDataDetails,
BenchmarkResponse,
@@ -289,7 +290,7 @@ export class DataService {
public deleteActivities({ filters }) {
const params = this.buildFiltersAsQueryParams({ filters });
- return this.http.delete(`/api/v1/order`, { params });
+ return this.http.delete('/api/v1/order', { params });
}
public deleteActivity(aId: string) {
@@ -636,36 +637,40 @@ export class DataService {
}
public loginAnonymous(accessToken: string) {
- return this.http.post(`/api/v1/auth/anonymous`, {
+ return this.http.post('/api/v1/auth/anonymous', {
accessToken
});
}
public postAccess(aAccess: CreateAccessDto) {
- return this.http.post(`/api/v1/access`, aAccess);
+ return this.http.post('/api/v1/access', aAccess);
}
public postAccount(aAccount: CreateAccountDto) {
- return this.http.post(`/api/v1/account`, aAccount);
+ return this.http.post('/api/v1/account', aAccount);
}
public postAccountBalance(aAccountBalance: CreateAccountBalanceDto) {
return this.http.post(
- `/api/v1/account-balance`,
+ '/api/v1/account-balance',
aAccountBalance
);
}
+ public postApiKey() {
+ return this.http.post('/api/v1/api-keys', {});
+ }
+
public postBenchmark(benchmark: AssetProfileIdentifier) {
- return this.http.post(`/api/v1/benchmark`, benchmark);
+ return this.http.post('/api/v1/benchmark', benchmark);
}
public postOrder(aOrder: CreateOrderDto) {
- return this.http.post(`/api/v1/order`, aOrder);
+ return this.http.post('/api/v1/order', aOrder);
}
public postUser() {
- return this.http.post(`/api/v1/user`, {});
+ return this.http.post('/api/v1/user', {});
}
public putAccount(aAccount: UpdateAccountDto) {
@@ -692,7 +697,7 @@ export class DataService {
}
public putUserSetting(aData: UpdateUserSettingDto) {
- return this.http.put(`/api/v1/user/setting`, aData);
+ return this.http.put('/api/v1/user/setting', aData);
}
public redeemCoupon(couponCode: string) {
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 4d5ce66d0..344a1f965 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -39,6 +39,7 @@ import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface';
import type { Product } from './product';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
+import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface';
@@ -72,6 +73,7 @@ export {
AdminMarketDataDetails,
AdminMarketDataItem,
AdminUsers,
+ ApiKeyResponse,
AssetProfileIdentifier,
Benchmark,
BenchmarkMarketDataDetails,
diff --git a/libs/common/src/lib/interfaces/responses/api-key-response.interface.ts b/libs/common/src/lib/interfaces/responses/api-key-response.interface.ts
new file mode 100644
index 000000000..dace14a02
--- /dev/null
+++ b/libs/common/src/lib/interfaces/responses/api-key-response.interface.ts
@@ -0,0 +1,3 @@
+export interface ApiKeyResponse {
+ apiKey: string;
+}
diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts
index 1a81938b5..cfee1c9e8 100644
--- a/libs/common/src/lib/permissions.ts
+++ b/libs/common/src/lib/permissions.ts
@@ -9,6 +9,7 @@ export const permissions = {
createAccess: 'createAccess',
createAccount: 'createAccount',
createAccountBalance: 'createAccountBalance',
+ createApiKey: 'createApiKey',
createOrder: 'createOrder',
createPlatform: 'createPlatform',
createTag: 'createTag',
diff --git a/libs/ui/src/lib/membership-card/membership-card.component.html b/libs/ui/src/lib/membership-card/membership-card.component.html
index 02a4a03f7..37634b020 100644
--- a/libs/ui/src/lib/membership-card/membership-card.component.html
+++ b/libs/ui/src/lib/membership-card/membership-card.component.html
@@ -13,6 +13,25 @@
[showLabel]="false"
/>
Membership
diff --git a/libs/ui/src/lib/membership-card/membership-card.component.scss b/libs/ui/src/lib/membership-card/membership-card.component.scss
index a7cbce91a..270adc0f1 100644
--- a/libs/ui/src/lib/membership-card/membership-card.component.scss
+++ b/libs/ui/src/lib/membership-card/membership-card.component.scss
@@ -42,6 +42,12 @@
background-color: #1d2124;
border-radius: calc(var(--borderRadius) - var(--borderWidth));
color: rgba(var(--light-primary-text));
+ line-height: 1.2;
+
+ button {
+ color: rgba(var(--light-primary-text));
+ height: 1.5rem;
+ }
.heading {
font-size: 13px;
diff --git a/libs/ui/src/lib/membership-card/membership-card.component.ts b/libs/ui/src/lib/membership-card/membership-card.component.ts
index b19072946..5d05d6fe5 100644
--- a/libs/ui/src/lib/membership-card/membership-card.component.ts
+++ b/libs/ui/src/lib/membership-card/membership-card.component.ts
@@ -3,15 +3,18 @@ import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
Component,
- Input
+ EventEmitter,
+ Input,
+ Output
} from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { GfLogoComponent } from '../logo';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [CommonModule, GfLogoComponent, RouterModule],
+ imports: [CommonModule, GfLogoComponent, MatButtonModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-membership-card',
standalone: true,
@@ -20,7 +23,17 @@ import { GfLogoComponent } from '../logo';
})
export class GfMembershipCardComponent {
@Input() public expiresAt: string;
+ @Input() public hasPermissionToCreateApiKey: boolean;
@Input() public name: string;
+ @Output() generateApiKeyClicked = new EventEmitter
();
+
public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
+
+ public onGenerateApiKey(event: MouseEvent) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.generateApiKeyClicked.emit();
+ }
}
diff --git a/package-lock.json b/package-lock.json
index ae7b60a44..0b93a63b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -83,6 +83,7 @@
"papaparse": "5.3.1",
"passport": "0.7.0",
"passport-google-oauth20": "2.0.0",
+ "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
@@ -28414,6 +28415,16 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/passport-headerapikey": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz",
+ "integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.15",
+ "passport-strategy": "^1.0.0"
+ }
+ },
"node_modules/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
diff --git a/package.json b/package.json
index 7f124ea20..24e0d64a4 100644
--- a/package.json
+++ b/package.json
@@ -129,6 +129,7 @@
"papaparse": "5.3.1",
"passport": "0.7.0",
"passport-google-oauth20": "2.0.0",
+ "passport-headerapikey": "1.2.2",
"passport-jwt": "4.0.1",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
From 1be0a6441787c9b95e8571b76add7aa3c1976db1 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 15:32:47 +0100
Subject: [PATCH 04/12] Feature/upgrade prettier to version 3.4.2 (#4090)
* Upgrade prettier to version 3.4.2
* 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 fcefd5644..d4043459f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the labels of the assistant
- Improved the caching of the portfolio snapshot in the portfolio calculator by expiring cache entries immediately in case of errors
- Extracted the historical market data editor to a reusable component
+- Upgraded `prettier` from version `3.3.3` to `3.4.2`
## 2.125.0 - 2024-11-30
diff --git a/package-lock.json b/package-lock.json
index 0b93a63b8..be8e0a412 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -149,7 +149,7 @@
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.1.0",
"nx": "20.1.2",
- "prettier": "3.3.3",
+ "prettier": "3.4.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.0.0",
"react": "18.2.0",
@@ -29419,9 +29419,9 @@
}
},
"node_modules/prettier": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
- "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
diff --git a/package.json b/package.json
index 24e0d64a4..668422621 100644
--- a/package.json
+++ b/package.json
@@ -195,7 +195,7 @@
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.1.0",
"nx": "20.1.2",
- "prettier": "3.3.3",
+ "prettier": "3.4.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.0.0",
"react": "18.2.0",
From 0e016745527860ecc26a16fc551da489996f9f5b Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 15:44:53 +0100
Subject: [PATCH 05/12] Feature/set hashedKey of ApiKey to unique (#4103)
* Set hashedKey to unique
---
apps/api/src/services/api-key/api-key.service.ts | 2 +-
.../migration.sql | 5 +++++
prisma/schema.prisma | 3 +--
3 files changed, 7 insertions(+), 3 deletions(-)
create mode 100644 prisma/migrations/20241207142023_set_hashed_key_of_api_key_to_unique/migration.sql
diff --git a/apps/api/src/services/api-key/api-key.service.ts b/apps/api/src/services/api-key/api-key.service.ts
index 2a1f14d03..f70e5330c 100644
--- a/apps/api/src/services/api-key/api-key.service.ts
+++ b/apps/api/src/services/api-key/api-key.service.ts
@@ -32,7 +32,7 @@ export class ApiKeyService {
public async getUserByApiKey(apiKey: string) {
const hashedKey = this.hashApiKey(apiKey);
- const { user } = await this.prismaService.apiKey.findFirst({
+ const { user } = await this.prismaService.apiKey.findUnique({
include: { user: true },
where: { hashedKey }
});
diff --git a/prisma/migrations/20241207142023_set_hashed_key_of_api_key_to_unique/migration.sql b/prisma/migrations/20241207142023_set_hashed_key_of_api_key_to_unique/migration.sql
new file mode 100644
index 000000000..f9a6eecbb
--- /dev/null
+++ b/prisma/migrations/20241207142023_set_hashed_key_of_api_key_to_unique/migration.sql
@@ -0,0 +1,5 @@
+-- DropIndex
+DROP INDEX "ApiKey_hashedKey_idx";
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_hashedKey_key" ON "ApiKey"("hashedKey");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e2587acf7..7df28d694 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -79,13 +79,12 @@ model Analytics {
model ApiKey {
createdAt DateTime @default(now())
- hashedKey String
+ hashedKey String @unique
id String @id @default(uuid())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], onDelete: Cascade, references: [id])
- @@index([hashedKey])
@@index([userId])
}
From 758a52087d7e58ac8c3809ad5d9aaf3f7205bbaf Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 15:51:55 +0100
Subject: [PATCH 06/12] Feature/upgrade prisma to version 6.0.1 (#4104)
* Upgrade prisma to version 6.0.1
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 54 +++++++++++++++++++++++------------------------
package.json | 4 ++--
3 files changed, 30 insertions(+), 29 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4043459f..42ab80c84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the caching of the portfolio snapshot in the portfolio calculator by expiring cache entries immediately in case of errors
- Extracted the historical market data editor to a reusable component
- Upgraded `prettier` from version `3.3.3` to `3.4.2`
+- Upgraded `prisma` from version `6.0.0` to `6.0.1`
## 2.125.0 - 2024-11-30
diff --git a/package-lock.json b/package-lock.json
index be8e0a412..212714bab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,7 +40,7 @@
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
- "@prisma/client": "6.0.0",
+ "@prisma/client": "6.0.1",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
"@stripe/stripe-js": "4.9.0",
@@ -151,7 +151,7 @@
"nx": "20.1.2",
"prettier": "3.4.2",
"prettier-plugin-organize-attributes": "1.0.0",
- "prisma": "6.0.0",
+ "prisma": "6.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "7.0.1",
@@ -8353,9 +8353,9 @@
"license": "MIT"
},
"node_modules/@prisma/client": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.0.tgz",
- "integrity": "sha512-tOBhG35ozqZ/5Y6B0TNOa6cwULUW8ijXqBXcgb12bfozqf6eGMyGs+jphywCsj6uojv5lAZZnxVSoLMVebIP+g==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.1.tgz",
+ "integrity": "sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@@ -8371,24 +8371,24 @@
}
},
"node_modules/@prisma/debug": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.0.tgz",
- "integrity": "sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.1.tgz",
+ "integrity": "sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.0.tgz",
- "integrity": "sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.1.tgz",
+ "integrity": "sha512-4hxzI+YQIR2uuDyVsDooFZGu5AtixbvM2psp+iayDZ4hRrAHo/YwgA17N23UWq7G6gRu18NvuNMb48qjP3DPQw==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.0.0",
+ "@prisma/debug": "6.0.1",
"@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e",
- "@prisma/fetch-engine": "6.0.0",
- "@prisma/get-platform": "6.0.0"
+ "@prisma/fetch-engine": "6.0.1",
+ "@prisma/get-platform": "6.0.1"
}
},
"node_modules/@prisma/engines-version": {
@@ -8399,25 +8399,25 @@
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.0.tgz",
- "integrity": "sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.1.tgz",
+ "integrity": "sha512-T36bWFVGeGYYSyYOj9d+O9G3sBC+pAyMC+jc45iSL63/Haq1GrYjQPgPMxrEj9m739taXrupoysRedQ+VyvM/Q==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.0.0",
+ "@prisma/debug": "6.0.1",
"@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e",
- "@prisma/get-platform": "6.0.0"
+ "@prisma/get-platform": "6.0.1"
}
},
"node_modules/@prisma/get-platform": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.0.tgz",
- "integrity": "sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.1.tgz",
+ "integrity": "sha512-zspC9vlxAqx4E6epMPMLLBMED2VD8axDe8sPnquZ8GOsn6tiacWK0oxrGK4UAHYzYUVuMVUApJbdXB2dFpLhvg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "6.0.0"
+ "@prisma/debug": "6.0.1"
}
},
"node_modules/@redis/bloom": {
@@ -29500,14 +29500,14 @@
}
},
"node_modules/prisma": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.0.tgz",
- "integrity": "sha512-RX7KtbW7IoEByf7MR32JK1PkVYLVYFqeODTtiIX3cqekq1aKdsF3Eud4zp2sUShMLjvdb5Jow0LbUjRq5LVxPw==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.1.tgz",
+ "integrity": "sha512-CaMNFHkf+DDq8zq3X/JJsQ4Koy7dyWwwtOKibkT/Am9j/tDxcfbg7+lB1Dzhx18G/+RQCMgjPYB61bhRqteNBQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/engines": "6.0.0"
+ "@prisma/engines": "6.0.1"
},
"bin": {
"prisma": "build/index.js"
diff --git a/package.json b/package.json
index 668422621..10ade46de 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
- "@prisma/client": "6.0.0",
+ "@prisma/client": "6.0.1",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
"@stripe/stripe-js": "4.9.0",
@@ -197,7 +197,7 @@
"nx": "20.1.2",
"prettier": "3.4.2",
"prettier-plugin-organize-attributes": "1.0.0",
- "prisma": "6.0.0",
+ "prisma": "6.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "7.0.1",
From 17ffb29275505fda5b670333dc4da0646674a3ff Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 15:54:29 +0100
Subject: [PATCH 07/12] Release 2.126.0 (#4105)
---
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 42ab80c84..b162d220e 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
+## 2.126.0 - 2024-12-07
### Added
diff --git a/package-lock.json b/package-lock.json
index 212714bab..4957619e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.125.0",
+ "version": "2.126.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.125.0",
+ "version": "2.126.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 10ade46de..f86c028b7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.125.0",
+ "version": "2.126.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From ebef361d638e75c64f966408d5385b0d07620745 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 21:41:21 +0100
Subject: [PATCH 08/12] Bugfix/fix exception in portfolio calculator (#4109)
* Fix exception in portfolio calculator
* Update changelog
---
CHANGELOG.md | 6 ++++++
.../src/app/portfolio/calculator/portfolio-calculator.ts | 1 +
2 files changed, 7 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b162d220e..61d373992 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Fixed
+
+- Fixed an exception in the caching of the portfolio snapshot in the portfolio calculator
+
## 2.126.0 - 2024-12-07
### Added
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index dec0e6387..eb18b3584 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -176,6 +176,7 @@ export abstract class PortfolioCalculator {
if (!transactionPoints.length) {
return {
currentValueInBaseCurrency: new Big(0),
+ errors: [],
hasErrors: false,
historicalData: [],
positions: [],
From d6357487eae6adddeb5974967574b7179d1f26a9 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 7 Dec 2024 21:43:30 +0100
Subject: [PATCH 09/12] Release 2.126.1 (#4110)
---
CHANGELOG.md | 8 +-------
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61d373992..2e8e4a73c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +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
-
-### Fixed
-
-- Fixed an exception in the caching of the portfolio snapshot in the portfolio calculator
-
-## 2.126.0 - 2024-12-07
+## 2.126.1 - 2024-12-07
### Added
diff --git a/package-lock.json b/package-lock.json
index 4957619e4..ecbc91c56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.126.0",
+ "version": "2.126.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.126.0",
+ "version": "2.126.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index f86c028b7..738f2db8a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.126.0",
+ "version": "2.126.1",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From 291be3e6051d89e50adb70ca49337dfa90703015 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 8 Dec 2024 08:05:08 +0100
Subject: [PATCH 10/12] Feature/extend X-ray page by summary (#4107)
* Add summary to X-ray page
* Update changelog
---
CHANGELOG.md | 6 +
.../src/app/portfolio/portfolio.controller.ts | 4 +-
.../src/app/portfolio/portfolio.service.ts | 164 ++++++++++--------
.../portfolio/x-ray/x-ray-page.component.html | 31 +++-
.../portfolio/x-ray/x-ray-page.component.ts | 52 +++---
apps/client/src/app/services/data.service.ts | 4 +-
libs/common/src/lib/interfaces/index.ts | 4 +-
.../interfaces/portfolio-report.interface.ts | 5 -
.../responses/portfolio-report.interface.ts | 9 +
9 files changed, 162 insertions(+), 117 deletions(-)
delete mode 100644 libs/common/src/lib/interfaces/portfolio-report.interface.ts
create mode 100644 libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e8e4a73c..7c20027c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Added
+
+- Extended the _X-ray_ page by a summary
+
## 2.126.1 - 2024-12-07
### Added
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index f2415dff3..b15d22268 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -23,7 +23,7 @@ import {
PortfolioHoldingsResponse,
PortfolioInvestments,
PortfolioPerformanceResponse,
- PortfolioReport
+ PortfolioReportResponse
} from '@ghostfolio/common/interfaces';
import {
hasReadRestrictedAccessPermission,
@@ -611,7 +611,7 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getReport(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
- ): Promise {
+ ): Promise {
const report = await this.portfolioService.getReport(impersonationId);
if (
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 15ca227e9..d16a52544 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -37,7 +37,7 @@ import {
PortfolioInvestments,
PortfolioPerformanceResponse,
PortfolioPosition,
- PortfolioReport,
+ PortfolioReportResponse,
PortfolioSummary,
Position,
UserSettings
@@ -1162,7 +1162,9 @@ export class PortfolioService {
};
}
- public async getReport(impersonationId: string): Promise {
+ public async getReport(
+ impersonationId: string
+ ): Promise {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const userSettings = this.request.user.Settings.settings as UserSettings;
@@ -1179,79 +1181,79 @@ export class PortfolioService {
})
).toNumber();
- return {
- rules: {
- accountClusterRisk:
- summary.ordersCount > 0
- ? await this.rulesService.evaluate(
- [
- new AccountClusterRiskCurrentInvestment(
- this.exchangeRateDataService,
- accounts
- ),
- new AccountClusterRiskSingleAccount(
- this.exchangeRateDataService,
- accounts
- )
- ],
- userSettings
- )
- : undefined,
- economicMarketClusterRisk:
- summary.ordersCount > 0
- ? await this.rulesService.evaluate(
- [
- new EconomicMarketClusterRiskDevelopedMarkets(
- this.exchangeRateDataService,
- marketsTotalInBaseCurrency,
- markets.developedMarkets.valueInBaseCurrency
- ),
- new EconomicMarketClusterRiskEmergingMarkets(
- this.exchangeRateDataService,
- marketsTotalInBaseCurrency,
- markets.emergingMarkets.valueInBaseCurrency
- )
- ],
- userSettings
- )
- : undefined,
- currencyClusterRisk:
- summary.ordersCount > 0
- ? await this.rulesService.evaluate(
- [
- new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
- this.exchangeRateDataService,
- Object.values(holdings)
- ),
- new CurrencyClusterRiskCurrentInvestment(
- this.exchangeRateDataService,
- Object.values(holdings)
- )
- ],
- userSettings
- )
- : undefined,
- emergencyFund: await this.rulesService.evaluate(
- [
- new EmergencyFundSetup(
- this.exchangeRateDataService,
- userSettings.emergencyFund
+ const rules: PortfolioReportResponse['rules'] = {
+ accountClusterRisk:
+ summary.ordersCount > 0
+ ? await this.rulesService.evaluate(
+ [
+ new AccountClusterRiskCurrentInvestment(
+ this.exchangeRateDataService,
+ accounts
+ ),
+ new AccountClusterRiskSingleAccount(
+ this.exchangeRateDataService,
+ accounts
+ )
+ ],
+ userSettings
)
- ],
- userSettings
- ),
- fees: await this.rulesService.evaluate(
- [
- new FeeRatioInitialInvestment(
- this.exchangeRateDataService,
- summary.committedFunds,
- summary.fees
+ : undefined,
+ economicMarketClusterRisk:
+ summary.ordersCount > 0
+ ? await this.rulesService.evaluate(
+ [
+ new EconomicMarketClusterRiskDevelopedMarkets(
+ this.exchangeRateDataService,
+ marketsTotalInBaseCurrency,
+ markets.developedMarkets.valueInBaseCurrency
+ ),
+ new EconomicMarketClusterRiskEmergingMarkets(
+ this.exchangeRateDataService,
+ marketsTotalInBaseCurrency,
+ markets.emergingMarkets.valueInBaseCurrency
+ )
+ ],
+ userSettings
)
- ],
- userSettings
- )
- }
+ : undefined,
+ currencyClusterRisk:
+ summary.ordersCount > 0
+ ? await this.rulesService.evaluate(
+ [
+ new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
+ this.exchangeRateDataService,
+ Object.values(holdings)
+ ),
+ new CurrencyClusterRiskCurrentInvestment(
+ this.exchangeRateDataService,
+ Object.values(holdings)
+ )
+ ],
+ userSettings
+ )
+ : undefined,
+ emergencyFund: await this.rulesService.evaluate(
+ [
+ new EmergencyFundSetup(
+ this.exchangeRateDataService,
+ userSettings.emergencyFund
+ )
+ ],
+ userSettings
+ ),
+ fees: await this.rulesService.evaluate(
+ [
+ new FeeRatioInitialInvestment(
+ this.exchangeRateDataService,
+ summary.committedFunds,
+ summary.fees
+ )
+ ],
+ userSettings
+ )
};
+
+ return { rules, statistics: this.getReportStatistics(rules) };
}
public async updateTags({
@@ -1670,6 +1672,24 @@ export class PortfolioService {
return { markets, marketsAdvanced };
}
+ private getReportStatistics(
+ evaluatedRules: PortfolioReportResponse['rules']
+ ): PortfolioReportResponse['statistics'] {
+ const rulesActiveCount = Object.values(evaluatedRules)
+ .flat()
+ .filter(({ isActive }) => {
+ return isActive === true;
+ }).length;
+
+ const rulesFulfilledCount = Object.values(evaluatedRules)
+ .flat()
+ .filter(({ value }) => {
+ return value === true;
+ }).length;
+
+ return { rulesActiveCount, rulesFulfilledCount };
+ }
+
private getStreaks({
investments,
savingsRate
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
index cd03b49bb..7a0a3512c 100644
--- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
@@ -2,11 +2,28 @@
X-ray
-
+
Ghostfolio X-ray uses static analysis to uncover potential issues and
risks in your portfolio. Adjust the rules below and set custom
thresholds to align with your personal investment strategy.
+
+ @if (isLoading) {
+
+ } @else {
+ {{ statistics?.rulesFulfilledCount }}
+ of
+ {{ statistics?.rulesActiveCount }}
+ rules are currently fulfilled.
+ }
+
Emergency Fund
@@ -20,7 +37,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="emergencyFundRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
@@ -39,7 +56,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="currencyClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
@@ -58,7 +75,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="accountClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
@@ -77,7 +94,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
@@ -96,7 +113,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="feeRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
@@ -111,7 +128,7 @@
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
- [isLoading]="isLoadingPortfolioReport"
+ [isLoading]="isLoading"
[rules]="inactiveRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
index 36f42fc3e..86bc37737 100644
--- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
@@ -3,8 +3,8 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
- PortfolioReportRule,
- PortfolioReport
+ PortfolioReportResponse,
+ PortfolioReportRule
} from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces/user.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@@ -26,7 +26,8 @@ export class XRayPageComponent {
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[];
- public isLoadingPortfolioReport = false;
+ public isLoading = false;
+ public statistics: PortfolioReportResponse['statistics'];
public user: User;
private unsubscribeSubject = new Subject();
@@ -87,56 +88,53 @@ export class XRayPageComponent {
}
private initializePortfolioReport() {
- this.isLoadingPortfolioReport = true;
+ this.isLoading = true;
this.dataService
.fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((portfolioReport) => {
- this.inactiveRules = this.mergeInactiveRules(portfolioReport);
+ .subscribe(({ rules, statistics }) => {
+ this.inactiveRules = this.mergeInactiveRules(rules);
+ this.statistics = statistics;
this.accountClusterRiskRules =
- portfolioReport.rules['accountClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
+ rules['accountClusterRisk']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
this.currencyClusterRiskRules =
- portfolioReport.rules['currencyClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
+ rules['currencyClusterRisk']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
this.economicMarketClusterRiskRules =
- portfolioReport.rules['economicMarketClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
+ rules['economicMarketClusterRisk']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
this.emergencyFundRules =
- portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
+ rules['emergencyFund']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.feeRules =
- portfolioReport.rules['fees']?.filter(({ isActive }) => {
+ rules['fees']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
- this.isLoadingPortfolioReport = false;
+ this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
- private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
+ private mergeInactiveRules(
+ rules: PortfolioReportResponse['rules']
+ ): PortfolioReportRule[] {
let inactiveRules: PortfolioReportRule[] = [];
- for (const category in report.rules) {
- const rulesArray = report.rules[category];
+ for (const category in rules) {
+ const rulesArray = rules[category];
inactiveRules = inactiveRules.concat(
rulesArray.filter(({ isActive }) => {
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 92d030827..eef258a5c 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -37,7 +37,7 @@ import {
PortfolioHoldingsResponse,
PortfolioInvestments,
PortfolioPerformanceResponse,
- PortfolioReport,
+ PortfolioReportResponse,
PublicPortfolioResponse,
User
} from '@ghostfolio/common/interfaces';
@@ -613,7 +613,7 @@ export class DataService {
}
public fetchPortfolioReport() {
- return this.http.get('/api/v1/portfolio/report');
+ return this.http.get('/api/v1/portfolio/report');
}
public fetchPublicPortfolio(aAccessId: string) {
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 344a1f965..ed8fd4f2a 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -34,7 +34,6 @@ import type { PortfolioOverview } from './portfolio-overview.interface';
import type { PortfolioPerformance } from './portfolio-performance.interface';
import type { PortfolioPosition } from './portfolio-position.interface';
import type { PortfolioReportRule } from './portfolio-report-rule.interface';
-import type { PortfolioReport } from './portfolio-report.interface';
import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface';
import type { Product } from './product';
@@ -50,6 +49,7 @@ import type { LookupResponse } from './responses/lookup-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
+import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
import type { QuotesResponse } from './responses/quotes-response.interface';
import type { ScraperConfiguration } from './scraper-configuration.interface';
@@ -108,7 +108,7 @@ export {
PortfolioPerformance,
PortfolioPerformanceResponse,
PortfolioPosition,
- PortfolioReport,
+ PortfolioReportResponse,
PortfolioReportRule,
PortfolioSummary,
Position,
diff --git a/libs/common/src/lib/interfaces/portfolio-report.interface.ts b/libs/common/src/lib/interfaces/portfolio-report.interface.ts
deleted file mode 100644
index a33a0aae6..000000000
--- a/libs/common/src/lib/interfaces/portfolio-report.interface.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PortfolioReportRule } from './portfolio-report-rule.interface';
-
-export interface PortfolioReport {
- rules: { [group: string]: PortfolioReportRule[] };
-}
diff --git a/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts
new file mode 100644
index 000000000..35ff033eb
--- /dev/null
+++ b/libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts
@@ -0,0 +1,9 @@
+import { PortfolioReportRule } from '../portfolio-report-rule.interface';
+
+export interface PortfolioReportResponse {
+ rules: { [group: string]: PortfolioReportRule[] };
+ statistics: {
+ rulesActiveCount: number;
+ rulesFulfilledCount: number;
+ };
+}
From 0841f8bd5ba844f8ee1c81a79bc5bb7498d3be89 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 8 Dec 2024 18:57:22 +0100
Subject: [PATCH 11/12] Bugfix/fix exception in portfolio calculator (#4113)
* Fix exception in portfolio calculator
* Update changelog
---
CHANGELOG.md | 4 ++++
.../src/app/portfolio/calculator/twr/portfolio-calculator.ts | 1 +
libs/common/src/lib/models/portfolio-snapshot.ts | 2 +-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c20027c8..637da61e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extended the _X-ray_ page by a summary
+### Fixed
+
+- Fixed an exception in the caching of the portfolio snapshot in the portfolio calculator
+
## 2.126.1 - 2024-12-07
### Added
diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
index 6c0d230b0..3f53ee04b 100644
--- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
@@ -101,6 +101,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
totalInterestWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect,
+ errors: [],
historicalData: [],
totalLiabilitiesWithCurrencyEffect: new Big(0),
totalValuablesWithCurrencyEffect: new Big(0)
diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts
index e51931711..46bd5c18f 100644
--- a/libs/common/src/lib/models/portfolio-snapshot.ts
+++ b/libs/common/src/lib/models/portfolio-snapshot.ts
@@ -13,7 +13,7 @@ export class PortfolioSnapshot {
@Type(() => Big)
currentValueInBaseCurrency: Big;
- errors?: AssetProfileIdentifier[];
+ errors: AssetProfileIdentifier[];
hasErrors: boolean;
From 618a918423d8f549ffbb258558c8ec929a07f713 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 8 Dec 2024 18:59:33 +0100
Subject: [PATCH 12/12] Release 2.127.0 (#4114)
---
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 637da61e6..f4771dc63 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
+## 2.127.0 - 2024-12-08
### Added
diff --git a/package-lock.json b/package-lock.json
index ecbc91c56..9db369481 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.126.1",
+ "version": "2.127.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.126.1",
+ "version": "2.127.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 738f2db8a..598bc40b3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.126.1",
+ "version": "2.127.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",