From 9217ff587b0528e74cd733d3fac82498f4e554a3 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:45:49 +0200
Subject: [PATCH 1/5] Task/harmonize subject of mailto links (#7268)
Harmonize subject of mailto links
---
.../components/admin-settings/admin-settings.component.html | 2 +-
.../holding-detail-dialog/holding-detail-dialog.component.ts | 2 +-
.../user-account-membership.component.ts | 2 +-
apps/client/src/app/pages/faq/saas/saas-page.html | 2 +-
apps/client/src/app/pages/pricing/pricing-page.html | 4 ++--
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.html b/apps/client/src/app/components/admin-settings/admin-settings.component.html
index 76af96c4e..8ee6250e5 100644
--- a/apps/client/src/app/components/admin-settings/admin-settings.component.html
+++ b/apps/client/src/app/components/admin-settings/admin-settings.component.html
@@ -21,7 +21,7 @@
Get Access
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
index b2cdedbc5..c5beb6c2d 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
@@ -456,7 +456,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.assetProfile?.symbol ? ` (${this.assetProfile.symbol})` : ''
}`;
- this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
+ this.reportDataGlitchMail = `mailto:hi@ghostfol.io?subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
if (this.assetProfile?.assetClass) {
this.assetClass = translate(this.assetProfile?.assetClass);
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
index 1b23a6df9..d086867aa 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
@@ -52,7 +52,7 @@ export class GfUserAccountMembershipComponent {
public priceId: string;
public routerLinkPricing = publicRoutes.pricing.routerLink;
public trySubscriptionMail =
- 'mailto:hi@ghostfol.io?Subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
+ 'mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards';
public user: User;
public constructor(
diff --git a/apps/client/src/app/pages/faq/saas/saas-page.html b/apps/client/src/app/pages/faq/saas/saas-page.html
index e832676bf..fc39acc53 100644
--- a/apps/client/src/app/pages/faq/saas/saas-page.html
+++ b/apps/client/src/app/pages/faq/saas/saas-page.html
@@ -96,7 +96,7 @@
Request your student discount
- here with
+ here with
your university e-mail address.
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html
index 23693457c..ddd92892a 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.html
+++ b/apps/client/src/app/pages/pricing/pricing-page.html
@@ -332,7 +332,7 @@
}
please
- contact us
@@ -343,7 +343,7 @@
Request it
- here
+ here
with your university e-mail address.
From 6278cf6691c5f09c7a5df81d0580330064948a0e Mon Sep 17 00:00:00 2001
From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Wed, 8 Jul 2026 02:47:38 +0700
Subject: [PATCH 2/5] Task/improve type safety in portfolio performance
component (#7265)
Improve type safety
---
.../home-overview/home-overview.html | 1 -
.../portfolio-performance.component.html | 22 ++--
.../portfolio-performance.component.ts | 103 ++++++++++--------
3 files changed, 68 insertions(+), 58 deletions(-)
diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html
index 8361c5b88..0ca4912b9 100644
--- a/apps/client/src/app/components/home-overview/home-overview.html
+++ b/apps/client/src/app/components/home-overview/home-overview.html
@@ -86,7 +86,6 @@
- @if (errors?.length > 0 && !isLoading) {
+ @if (errors()?.length > 0 && !isLoading()) {
}
- @if (isLoading) {
+ @if (isLoading()) {
- {{ unit }}
+ {{ unit() }}
- @if (showDetails) {
+ @if (showDetails()) {
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
index 56e75ec1e..a48d77a2d 100644
--- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
+++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
@@ -13,10 +13,11 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import {
ChangeDetectionStrategy,
Component,
+ effect,
ElementRef,
- Input,
- OnChanges,
- ViewChild
+ inject,
+ input,
+ viewChild
} from '@angular/core';
import { IonIcon } from '@ionic/angular/standalone';
import { CountUp } from 'countup.js';
@@ -32,58 +33,68 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
styleUrls: ['./portfolio-performance.component.scss'],
templateUrl: './portfolio-performance.component.html'
})
-export class GfPortfolioPerformanceComponent implements OnChanges {
- @Input() deviceType: string;
- @Input() errors: ResponseError['errors'];
- @Input() isLoading: boolean;
- @Input() locale = getLocale();
- @Input() performance: PortfolioPerformance;
- @Input() precision: number;
- @Input() showDetails: boolean;
- @Input() unit: string;
+export class GfPortfolioPerformanceComponent {
+ public readonly errors = input();
+ public readonly isLoading = input();
+ public readonly locale = input(getLocale());
+ public readonly performance = input.required();
+ public readonly precision = input.required({
+ transform: (value) => {
+ return value >= 0 ? value : 2;
+ }
+ });
+ public readonly showDetails = input(false);
+ public readonly unit = input.required();
- @ViewChild('value') value: ElementRef;
+ private readonly value =
+ viewChild.required>('value');
- public constructor(private notificationService: NotificationService) {
- addIcons({ timeOutline });
- }
+ private readonly notificationService = inject(NotificationService);
- public ngOnChanges() {
- this.precision = this.precision >= 0 ? this.precision : 2;
+ public constructor() {
+ addIcons({ timeOutline });
- if (this.isLoading) {
- if (this.value?.nativeElement) {
- this.value.nativeElement.innerHTML = '';
- }
- } else {
- if (isNumber(this.performance?.currentValueInBaseCurrency)) {
- new CountUp('value', this.performance?.currentValueInBaseCurrency, {
- decimal: getNumberFormatDecimal(this.locale),
- decimalPlaces: this.precision,
- duration: 1,
- separator: getNumberFormatGroup(this.locale)
- }).start();
- } else if (this.showDetails === false) {
- new CountUp(
- 'value',
- this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
- {
- decimal: getNumberFormatDecimal(this.locale),
- decimalPlaces: 2,
- duration: 1,
- separator: getNumberFormatGroup(this.locale)
- }
- ).start();
+ effect(() => {
+ if (this.isLoading()) {
+ if (this.value().nativeElement) {
+ this.value().nativeElement.innerHTML = '';
+ }
} else {
- this.value.nativeElement.innerHTML = '*****';
+ if (isNumber(this.performance().currentValueInBaseCurrency)) {
+ new CountUp('value', this.performance().currentValueInBaseCurrency, {
+ decimal: getNumberFormatDecimal(this.locale()),
+ decimalPlaces: this.precision(),
+ duration: 1,
+ separator: getNumberFormatGroup(this.locale())
+ }).start();
+ } else if (this.showDetails() === false) {
+ new CountUp(
+ 'value',
+ this.performance().netPerformancePercentageWithCurrencyEffect * 100,
+ {
+ decimal: getNumberFormatDecimal(this.locale()),
+ decimalPlaces: 2,
+ duration: 1,
+ separator: getNumberFormatGroup(this.locale())
+ }
+ ).start();
+ } else {
+ this.value().nativeElement.innerHTML = '*****';
+ }
}
- }
+ });
}
- public onShowErrors() {
- const errorMessageParts = [];
+ protected onShowErrors() {
+ const errors = this.errors();
+
+ if (!errors?.length) {
+ return;
+ }
+
+ const errorMessageParts: string[] = [];
- for (const error of this.errors) {
+ for (const error of errors) {
errorMessageParts.push(`${error.symbol} (${error.dataSource})`);
}
From 9d98799b47aa8182d21155cae85bbf68262a749f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:48:25 +0200
Subject: [PATCH 3/5] Task/reorder updatedAt field in symbol profile data model
(#7270)
Reorder updatedAt
---
prisma/schema.prisma | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 233a63688..ed07d6f47 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -203,11 +203,11 @@ model SymbolProfile {
isActive Boolean @default(true)
isin String?
name String?
- updatedAt DateTime @updatedAt
scraperConfiguration Json?
sectors Json?
symbol String
symbolMapping Json?
+ updatedAt DateTime @updatedAt
url String?
user User? @relation(fields: [userId], onDelete: Cascade, references: [id])
userId String?
From e9c48de4909034a9e118196fd6b6517706fb1e60 Mon Sep 17 00:00:00 2001
From: David Requeno <108202767+DavidReque@users.noreply.github.com>
Date: Tue, 7 Jul 2026 14:17:55 -0600
Subject: [PATCH 4/5] Task/set change detection strategy to OnPush in users of
admin control panel (#7266)
* Set change detection strategy to OnPush
* Update changelog
---
CHANGELOG.md | 1 +
.../src/app/components/admin-users/admin-users.component.ts | 6 ++++++
2 files changed, 7 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e968ebdba..6d127948f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Set the change detection strategy to `OnPush` in the _FIRE_ page
+- Set the change detection strategy to `OnPush` in the users section of the admin control panel
- Improved the language localization for Dutch (`nl`)
- Improved the language localization for French (`fr`)
- Improved the language localization for German (`de`)
diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts
index 5460745f5..4b9848cf8 100644
--- a/apps/client/src/app/components/admin-users/admin-users.component.ts
+++ b/apps/client/src/app/components/admin-users/admin-users.component.ts
@@ -27,6 +27,7 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
+ ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
@@ -67,6 +68,7 @@ import { interval } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfPremiumIndicatorComponent,
@@ -165,6 +167,8 @@ export class GfAdminUsersComponent implements OnInit {
this.user.permissions,
permissions.impersonateAllUsers
);
+
+ this.changeDetectorRef.markForCheck();
}
}),
switchMap(() => this.route.paramMap)
@@ -196,6 +200,8 @@ export class GfAdminUsersComponent implements OnInit {
pageIndex: this.paginator().pageIndex,
showLoading: false
});
+
+ this.changeDetectorRef.markForCheck();
});
}
From 45ab4016e521f7e9978a3c92eb93b9f24d1a4a74 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 7 Jul 2026 22:28:43 +0200
Subject: [PATCH 5/5] Task/improve user account deletion flow (#7269)
* Improve user account deletion flow
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/user/user.service.ts | 8 ++
.../user-account-settings.component.ts | 9 ++
.../user-account-settings.html | 110 +++++++++++-------
libs/common/src/lib/permissions.ts | 1 +
5 files changed, 84 insertions(+), 45 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d127948f..c111ac3c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Improved the user account deletion flow in the user settings of the user account page
- Set the change detection strategy to `OnPush` in the _FIRE_ page
- Set the change detection strategy to `OnPush` in the users section of the admin control panel
- Improved the language localization for Dutch (`nl`)
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 0c159bc1c..4dcf16034 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -535,6 +535,14 @@ export class UserService {
user.subscription.offer.label = undefined;
}
+ if (
+ !hasRole(user, Role.DEMO) &&
+ (user.provider !== 'ANONYMOUS' ||
+ user.subscription?.type === SubscriptionType.Premium)
+ ) {
+ currentPermissions.push(permissions.requestOwnUserDeletion);
+ }
+
if (hasRole(user, Role.ADMIN)) {
currentPermissions.push(permissions.syncDemoUserAccount);
}
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
index 948c5a25e..c8210b0c3 100644
--- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
+++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
@@ -71,11 +71,13 @@ import { catchError } from 'rxjs/operators';
export class GfUserAccountSettingsComponent implements OnInit {
public appearancePlaceholder = $localize`Auto`;
public baseCurrency: string;
+ public closeUserAccountMail: string;
public currencies: string[] = [];
public deleteOwnUserForm = this.formBuilder.group({
accessToken: ['', Validators.required]
});
public hasPermissionToDeleteOwnUser: boolean;
+ public hasPermissionToRequestOwnUserDeletion: boolean;
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public isAccessTokenHidden = true;
@@ -124,11 +126,18 @@ export class GfUserAccountSettingsComponent implements OnInit {
if (state?.user) {
this.user = state.user;
+ this.closeUserAccountMail = `mailto:hi@ghostfol.io?subject=Delete Account&body=Hello%0D%0DPlease delete my Ghostfolio account.%0D%0DUser ID: ${this.user.id}%0D%0DKind regards`;
+
this.hasPermissionToDeleteOwnUser = hasPermission(
this.user.permissions,
permissions.deleteOwnUser
);
+ this.hasPermissionToRequestOwnUserDeletion = hasPermission(
+ this.user.permissions,
+ permissions.requestOwnUserDeletion
+ );
+
this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions,
permissions.updateUserSettings
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html
index cefa3a0a1..351e718ec 100644
--- a/apps/client/src/app/components/user-account-settings/user-account-settings.html
+++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html
@@ -280,61 +280,81 @@
-
- @if (hasPermissionToDeleteOwnUser) {
+ @if (
+ hasPermissionToDeleteOwnUser || hasPermissionToRequestOwnUserDeletion
+ ) {
-
diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts
index f9cb19562..428de1788 100644
--- a/libs/common/src/lib/permissions.ts
+++ b/libs/common/src/lib/permissions.ts
@@ -50,6 +50,7 @@ export const permissions = {
readTags: 'readTags',
readWatchlist: 'readWatchlist',
reportDataGlitch: 'reportDataGlitch',
+ requestOwnUserDeletion: 'requestOwnUserDeletion',
syncDemoUserAccount: 'syncDemoUserAccount',
toggleReadOnlyMode: 'toggleReadOnlyMode',
updateAccount: 'updateAccount',