From d910fd40fc741c26fc1755affc208ec750334ddf Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:42:25 +0200 Subject: [PATCH] Feature/Ghostfolio AI in user settings (#8142) * Persist Ghostfolio AI in user settings * Update changelog --- CHANGELOG.md | 1 + .../app/endpoints/ai/ai.controller.spec.ts | 2 +- .../api/src/app/endpoints/ai/ai.controller.ts | 2 +- apps/api/src/app/user/user.controller.ts | 4 ++++ apps/api/src/app/user/user.service.ts | 7 ++++-- .../user-account-settings.component.ts | 22 +++++++++++++++++++ .../user-account-settings.html | 19 ++++++++++++++++ .../import-activities-dialog.component.ts | 18 ++++++++++++--- .../import-activities-dialog.html | 3 +-- .../src/lib/dtos/update-user-setting.dto.ts | 4 ++++ libs/common/src/lib/helper.ts | 1 + .../lib/interfaces/user-settings.interface.ts | 1 + libs/common/src/lib/permissions.ts | 2 +- 13 files changed, 76 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f624679d7c..fbb3bdcc97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added an option to enable _Ghostfolio AI_ in the user settings of the user account page (experimental) - Added an icon to indicate the client of the last request in the users table of the admin control panel ### Changed diff --git a/apps/api/src/app/endpoints/ai/ai.controller.spec.ts b/apps/api/src/app/endpoints/ai/ai.controller.spec.ts index 0f9550f693..b703bd64eb 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.spec.ts @@ -95,7 +95,7 @@ describe('AiController', () => { describe('getActivitiesFromFile', () => { it('Requires the permission and limits the requests of each user per week', () => { expect(getMetadataOfMethod(HAS_PERMISSION_KEY)).toBe( - permissions.importActivitiesWithAi + permissions.enableGhostfolioAi ); const guards = getMetadataOfMethod(GUARDS_METADATA); diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts index 4f0f2bb845..29812886df 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.ts @@ -73,7 +73,7 @@ export class AiController { * written for the caller gives the status 400 with this message, the * timeout gives the status 400 with a message for the caller as well. */ - @HasPermission(permissions.importActivitiesWithAi) + @HasPermission(permissions.enableGhostfolioAi) @HttpCode(StatusCodes.OK) @Post('activities') @Throttle({ diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index c4824fe7d6..60f184ed0f 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -240,6 +240,10 @@ export class UserController { } } + if (!userSettings.isExperimentalFeatures) { + delete userSettings.isGhostfolioAiEnabled; + } + return this.userService.updateUserSetting({ emitPortfolioChangedEvent, userSettings, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0e927dff09..c2f5ad5661 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -469,6 +469,9 @@ export class UserService { // Reset holdings view mode user.settings.settings.holdingsViewMode = undefined; + + // Reset Ghostfolio AI + user.settings.settings.isGhostfolioAiEnabled = undefined; } else if (user.subscription?.type === SubscriptionType.Premium) { if (!hasRole(user, Role.DEMO)) { currentPermissions.push(permissions.createApiKey); @@ -477,7 +480,7 @@ export class UserService { currentPermissions.push(permissions.reportDataGlitch); if ((user.settings.settings as UserSettings).isExperimentalFeatures) { - currentPermissions.push(permissions.importActivitiesWithAi); + currentPermissions.push(permissions.enableGhostfolioAi); } } @@ -527,9 +530,9 @@ export class UserService { if (isReadOnlyMode) { currentPermissions = currentPermissions.filter((permission) => { return !( + permission === permissions.enableGhostfolioAi || permission.startsWith('create') || permission.startsWith('delete') || - permission.startsWith('import') || permission.startsWith('update') ); }); 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 3c95652e44..b47818638f 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 @@ -93,6 +93,7 @@ export class GfUserAccountSettingsComponent implements OnInit { }); protected hasImpersonationId: boolean; protected hasPermissionToDeleteOwnUser: boolean; + protected hasPermissionToEnableGhostfolioAi: boolean; protected hasPermissionToRequestOwnUserDeletion: boolean; protected hasPermissionToUpdateOwnAccessToken: boolean; protected hasPermissionToUpdateViewMode: boolean; @@ -190,6 +191,11 @@ export class GfUserAccountSettingsComponent implements OnInit { permissions.deleteOwnUser ); + this.hasPermissionToEnableGhostfolioAi = hasPermission( + this.user.permissions, + permissions.enableGhostfolioAi + ); + this.hasPermissionToRequestOwnUserDeletion = hasPermission( this.user.permissions, permissions.requestOwnUserDeletion @@ -339,6 +345,22 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } + protected onGhostfolioAiChange(aEvent: MatSlideToggleChange) { + this.dataService + .putUserSetting({ isGhostfolioAiEnabled: aEvent.checked }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.userService + .get(true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + }); + } + protected onRestrictedViewChange(aEvent: MatSlideToggleChange) { this.dataService .putUserSetting({ isRestrictedView: aEvent.checked }) 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 bf18373447..6886baa46a 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 @@ -290,6 +290,25 @@ /> + @if (deviceType() !== 'mobile' && hasPermissionToEnableGhostfolioAi) { +
+
+
Ghostfolio AI
+
+ Data processing by a third-party AI provider +
+
+
+ +
+
+ }
Ghostfolio User ID diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index a40ed5096e..1d704c57c1 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -124,13 +124,20 @@ export class GfImportActivitiesDialogComponent { protected dialogTitle = $localize`Import Activities`; protected errorMessages: string[] = []; protected readonly ghostfolioAiTooltip = $localize`The content of your file will be processed by a third-party AI provider. Please remove any personal data before uploading.`; - protected readonly hasPermissionToImportActivitiesWithAi = hasPermission( + + protected readonly hasPermissionToEnableGhostfolioAi = hasPermission( this.data.user?.permissions, - permissions.importActivitiesWithAi + permissions.enableGhostfolioAi ); + protected holdings: PortfolioPosition[] = []; protected importStep: ImportStep = ImportStep.UPLOAD_FILE; - protected isGhostfolioAiEnabled = false; + + protected isGhostfolioAiEnabled = + this.data.deviceType !== 'mobile' && + this.hasPermissionToEnableGhostfolioAi && + this.data.user?.settings?.isGhostfolioAiEnabled === true; + protected isLoading = false; protected isMaxActivitiesOfGhostfolioAiReached = false; protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES; @@ -261,6 +268,11 @@ export class GfImportActivitiesDialogComponent { protected onGhostfolioAiChange({ checked }: MatCheckboxChange) { this.isGhostfolioAiEnabled = checked; + + this.dataService + .putUserSetting({ isGhostfolioAiEnabled: checked }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(); } protected onImportStepChange(event: StepperSelectionEvent) { diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index d1d4b24f4b..b0b2aabf20 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -125,8 +125,7 @@

@if ( - data.deviceType !== 'mobile' && - hasPermissionToImportActivitiesWithAi + data.deviceType !== 'mobile' && hasPermissionToEnableGhostfolioAi ) {