Browse Source

Merge branch 'main' into task/improve-mcp

pull/7786/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
538bd40ff7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      CHANGELOG.md
  2. 5
      apps/api/src/app/endpoints/ai/ai.controller.ts
  3. 2
      apps/api/src/app/endpoints/ai/ai.module.ts
  4. 6
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  5. 4
      apps/api/src/app/user/user.service.ts
  6. 30
      apps/api/src/dtos/filter.dto.ts
  7. 18
      apps/api/src/services/api/api.service.ts
  8. 2
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  9. 38
      apps/client/src/app/components/access-table/access-table.component.html
  10. 26
      apps/client/src/app/components/access-table/access-table.component.ts
  11. 2
      apps/client/src/app/components/home-holdings/home-holdings.html
  12. 42
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  13. 14
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
  14. 84
      apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts
  15. 3
      apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts
  16. 86
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 1
      apps/client/src/app/pages/accounts/accounts-page.html
  18. 9
      apps/client/src/app/pages/accounts/accounts-page.routes.ts
  19. 4
      apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts
  20. 52
      apps/client/src/locales/messages.it.xlf
  21. 20
      libs/common/src/lib/personal-finance-tools.ts
  22. 5
      libs/common/src/lib/routes/routes.ts
  23. 21
      libs/common/src/lib/scopes.ts
  24. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  25. 8
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  26. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  27. 120
      package-lock.json
  28. 12
      package.json

17
CHANGELOG.md

@ -10,10 +10,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the server of the Model Context Protocol (MCP) (experimental) - Improved the server of the Model Context Protocol (MCP) (experimental)
- Hardened the validation of the query parameters (`accounts`, `assetClasses`, `dataSource` and `tags`) in the endpoints with filters
### Fixed
- Fixed the missing country mapping of _Congo (Dem. Rep. of the)_ and _Congo (Rep. of)_ in the _Financial Modeling Prep_ service
## 3.66.0 - 2026-09-03
### Changed
- Moved the details of the granted access from the table to the dialog on the access page (experimental)
- Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental)
- Migrated the transfer cash balance dialog to a dedicated route
- Improved the language localization for Italian (`it`)
- Upgraded `@rekog/mcp-nest` from version `2.0.0` to `2.0.2`
- Upgraded `prisma` from version `7.9.1` to `7.10.0`
### Fixed ### Fixed
- Fixed the loading state of the accounts table on the accounts page - Fixed the loading state of the accounts table on the accounts page
- Fixed the loading state of the holdings table on the portfolio holdings page
## 3.65.0 - 2026-08-31 ## 3.65.0 - 2026-08-31

5
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -1,6 +1,7 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { FilterDto } from '@ghostfolio/api/dtos/filter.dto'; import { FilterDto } from '@ghostfolio/api/dtos/filter.dto';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { AiPromptResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -12,7 +13,8 @@ import {
Inject, Inject,
Param, Param,
Query, Query,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -30,6 +32,7 @@ export class AiController {
@Get('prompt/:mode') @Get('prompt/:mode')
@HasPermission(permissions.readAiPrompt) @HasPermission(permissions.readAiPrompt)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getPrompt( public async getPrompt(
@Param('mode') mode: AiPromptMode, @Param('mode') mode: AiPromptMode,
@Query() @Query()

2
apps/api/src/app/endpoints/ai/ai.module.ts

@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -46,6 +47,7 @@ import { AiService } from './ai.service';
RedisCacheModule, RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
TagModule, TagModule,
TransformDataSourceInRequestModule,
UserModule UserModule
], ],
providers: [ providers: [

6
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -64,8 +64,8 @@ export class GhostfolioMcpController {
}: z.infer<typeof GET_ACCOUNTS_PARAMETERS> }: z.infer<typeof GET_ACCOUNTS_PARAMETERS>
) { ) {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts: accountIds?.join(','), filterByAccounts: accountIds,
filterByAssetClasses: assetClasses?.join(','), filterByAssetClasses: assetClasses,
filterByDataSource: holding?.dataSource, filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol filterBySymbol: holding?.symbol
}); });
@ -111,7 +111,7 @@ export class GhostfolioMcpController {
} }
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetClasses: assetClasses?.join(','), filterByAssetClasses: assetClasses,
filterByDataSource: holding?.dataSource, filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol filterBySymbol: holding?.symbol
}); });

4
apps/api/src/app/user/user.service.ts

@ -275,7 +275,7 @@ export class UserService {
} }
try { try {
const { isBlocked } = await this.throttlerStorage.increment( const { totalHits } = await this.throttlerStorage.increment(
`${THROTTLE_DAILY_KEY}-${user.id}`, `${THROTTLE_DAILY_KEY}-${user.id}`,
THROTTLE_DAILY_TTL, THROTTLE_DAILY_TTL,
maxDailyRequests, maxDailyRequests,
@ -283,7 +283,7 @@ export class UserService {
THROTTLE_DAILY_KEY THROTTLE_DAILY_KEY
); );
return isBlocked; return totalHits > maxDailyRequests;
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);

30
apps/api/src/dtos/filter.dto.ts

@ -1,23 +1,35 @@
import { IsOptional, IsString } from 'class-validator'; import { AssetClass, DataSource } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
import { isString } from 'lodash';
export class FilterDto { export class FilterDto {
@IsOptional() @IsOptional()
@IsString() @IsUUID(undefined, { each: true })
accounts?: string; @Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
accounts?: string[];
@IsEnum(AssetClass, { each: true })
@IsOptional() @IsOptional()
@IsString() @Transform(({ value }: TransformFnParams) => {
assetClasses?: string; return isString(value) ? value.split(',') : value;
})
assetClasses?: AssetClass[];
@IsEnum(DataSource)
@IsOptional() @IsOptional()
@IsString() dataSource?: DataSource;
dataSource?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
symbol?: string; symbol?: string;
@IsOptional() @IsOptional()
@IsString() @IsUUID(undefined, { each: true })
tags?: string; @Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
tags?: string[];
} }

18
apps/api/src/services/api/api.service.ts

@ -14,23 +14,23 @@ export class ApiService {
filterBySymbol, filterBySymbol,
filterByTags filterByTags
}: { }: {
filterByAccounts?: string; filterByAccounts?: string[];
filterByAssetClasses?: string; filterByAssetClasses?: string[];
filterByAssetSubClasses?: string; filterByAssetSubClasses?: string;
filterByDataSource?: string; filterByDataSource?: string;
filterByHoldingType?: string; filterByHoldingType?: string;
filterBySearchQuery?: string; filterBySearchQuery?: string;
filterBySymbol?: string; filterBySymbol?: string;
filterByTags?: string; filterByTags?: string[];
}): Filter[] { }): Filter[] {
const accountIds = filterByAccounts?.split(',') ?? []; const accountIds = filterByAccounts ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? []; const assetClasses = filterByAssetClasses ?? [];
const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; const assetSubClasses = filterByAssetSubClasses?.split(',') ?? [];
const dataSource = filterByDataSource; const dataSource = filterByDataSource;
const holdingType = filterByHoldingType; const holdingType = filterByHoldingType;
const searchQuery = filterBySearchQuery?.toLowerCase(); const searchQuery = filterBySearchQuery?.toLowerCase();
const symbol = filterBySymbol; const symbol = filterBySymbol;
const tagIds = filterByTags?.split(',') ?? []; const tagIds = filterByTags ?? [];
const filters = [ const filters = [
...accountIds.map((accountId) => { ...accountIds.map((accountId) => {
@ -96,11 +96,11 @@ export class ApiService {
userSettings: UserSettings; userSettings: UserSettings;
}): Filter[] { }): Filter[] {
return this.buildFiltersFromQueryParams({ return this.buildFiltersFromQueryParams({
filterByAccounts: userSettings?.['filters.accounts']?.[0], filterByAccounts: userSettings?.['filters.accounts'],
filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0], filterByAssetClasses: userSettings?.['filters.assetClasses'],
filterByDataSource: userSettings?.['filters.dataSource'], filterByDataSource: userSettings?.['filters.dataSource'],
filterBySymbol: userSettings?.['filters.symbol'], filterBySymbol: userSettings?.['filters.symbol'],
filterByTags: userSettings?.['filters.tags']?.[0] filterByTags: userSettings?.['filters.tags']
}); });
} }
} }

2
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -54,6 +54,8 @@ export class FinancialModelingPrepService
implements DataProviderInterface, OnModuleInit implements DataProviderInterface, OnModuleInit
{ {
private static countriesMapping = { private static countriesMapping = {
'Congo (Dem. Rep. of the)': 'CD',
'Congo (Rep. of)': 'CG',
'Czech Republic': 'CZ', 'Czech Republic': 'CZ',
'Korea (the Republic of)': 'KR', 'Korea (the Republic of)': 'KR',
Macau: 'MO', Macau: 'MO',

38
apps/client/src/app/components/access-table/access-table.component.html

@ -45,36 +45,6 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="details">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
@if (element.type === 'PUBLIC') {
<div class="align-items-center d-flex">
<ion-icon class="mr-1" name="link-outline" />
<a target="_blank" [href]="getPublicUrl(element.id)">{{
getPublicUrl(element.id)
}}</a>
</div>
@if (user()?.settings?.isExperimentalFeatures) {
<div>
<code
>GET {{ baseUrl }}/api/v1/public/{{
element.id
}}/portfolio</code
>
</div>
}
} @else if (element.type === 'MCP' && hasPermissionToEnableMcp) {
<div>
<code>{{ baseUrl }}{{ mcpEndpoint }}</code>
</div>
<div>
<code>Authorization: Bearer {{ element.id }}</code>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell></th> <th *matHeaderCellDef class="px-1 text-right" mat-header-cell></th>
@ -113,14 +83,6 @@
</span> </span>
</button> </button>
} }
@if (element.type === 'MCP' && hasPermissionToEnableMcp) {
<button mat-menu-item (click)="onCopyIdToClipboard(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline" />
<span i18n>Copy identifier to clipboard</span>
</span>
</button>
}
@if ( @if (
(!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) || (!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) ||
element.type === 'PUBLIC' || element.type === 'PUBLIC' ||

26
apps/client/src/app/components/access-table/access-table.component.ts

@ -1,4 +1,3 @@
import { MCP_ENDPOINT } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
@ -32,7 +31,6 @@ import {
copyOutline, copyOutline,
createOutline, createOutline,
ellipsisHorizontal, ellipsisHorizontal,
linkOutline,
removeCircleOutline, removeCircleOutline,
trashOutline trashOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
@ -81,14 +79,7 @@ export class GfAccessTableComponent {
protected readonly dataSource = new MatTableDataSource<Access>(); protected readonly dataSource = new MatTableDataSource<Access>();
protected readonly displayedColumns = computed(() => { protected readonly displayedColumns = computed(() => {
const columns = [ const columns = ['alias', 'grantee', 'type', 'lastUsedAt', 'expiresAt'];
'alias',
'grantee',
'type',
'lastUsedAt',
'expiresAt',
'details'
];
if (this.showActions()) { if (this.showActions()) {
columns.push('actions'); columns.push('actions');
@ -109,8 +100,6 @@ export class GfAccessTableComponent {
return !this.accesses(); return !this.accesses();
}); });
protected readonly mcpEndpoint = MCP_ENDPOINT;
private readonly clipboard = inject(Clipboard); private readonly clipboard = inject(Clipboard);
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
@ -121,7 +110,6 @@ export class GfAccessTableComponent {
copyOutline, copyOutline,
createOutline, createOutline,
ellipsisHorizontal, ellipsisHorizontal,
linkOutline,
removeCircleOutline, removeCircleOutline,
trashOutline trashOutline
}); });
@ -142,18 +130,6 @@ export class GfAccessTableComponent {
return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`; return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`;
} }
protected onCopyIdToClipboard(aId: string) {
this.clipboard.copy(aId);
this.snackBar.open(
'✅ ' + $localize`Identifier has been copied to the clipboard`,
undefined,
{
duration: ms('3 seconds')
}
);
}
protected onCopyUrlToClipboard(aId: string) { protected onCopyUrlToClipboard(aId: string) {
this.clipboard.copy(this.getPublicUrl(aId)); this.clipboard.copy(this.getPublicUrl(aId));

2
apps/client/src/app/components/home-holdings/home-holdings.html

@ -37,7 +37,7 @@
} }
<div [class.d-none]="holdingsViewMode !== 'TABLE'"> <div [class.d-none]="holdingsViewMode !== 'TABLE'">
<gf-holdings-table <gf-holdings-table
[holdings]="holdings ?? []" [holdings]="holdings"
[locale]="user?.settings?.locale ?? DEFAULT_LOCALE" [locale]="user?.settings?.locale ?? DEFAULT_LOCALE"
(holdingClicked)="onHoldingClicked($event)" (holdingClicked)="onHoldingClicked($event)"
/> />

42
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -1,11 +1,12 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE, MCP_ENDPOINT } from '@ghostfolio/common/config';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { canApplyFiltersToAccess } from '@ghostfolio/common/helper'; import { canApplyFiltersToAccess } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { import {
Scope, Scope,
canGrantRestrictedWriteAccess,
getAccessLevel, getAccessLevel,
getScopesOfAccess, getScopesOfAccess,
getScopesOfAccessLevel, getScopesOfAccessLevel,
@ -26,6 +27,7 @@ import {
} from '@ghostfolio/ui/portfolio-filter-form'; } from '@ghostfolio/ui/portfolio-filter-form';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { JsonPipe } from '@angular/common';
import type { HttpErrorResponse } from '@angular/common/http'; import type { HttpErrorResponse } from '@angular/common/http';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -68,6 +70,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
FormsModule, FormsModule,
GfAccessLevelIconComponent, GfAccessLevelIconComponent,
GfPortfolioFilterFormComponent, GfPortfolioFilterFormComponent,
JsonPipe,
MatButtonModule, MatButtonModule,
MatDatepickerModule, MatDatepickerModule,
MatDialogModule, MatDialogModule,
@ -87,6 +90,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
public tags: Filter[] = []; public tags: Filter[] = [];
protected accessForm: FormGroup; protected accessForm: FormGroup;
protected readonly baseUrl = window.location.origin;
protected minExpiresAt: Date; protected minExpiresAt: Date;
protected readonly mode: 'create' | 'update'; protected readonly mode: 'create' | 'update';
protected readonly today = startOfDay(new Date()); protected readonly today = startOfDay(new Date());
@ -125,6 +129,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return this.hasExperimentalFeatures && this.hasPermissionToEnableMcp; return this.hasExperimentalFeatures && this.hasPermissionToEnableMcp;
} }
public get canGrantRestrictedWriteAccess() {
return canGrantRestrictedWriteAccess({ type: this.accessType });
}
public get canGrantWriteAccess() { public get canGrantWriteAccess() {
return this.hasExperimentalFeatures; return this.hasExperimentalFeatures;
} }
@ -217,6 +225,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.loadHoldings(); this.loadHoldings();
} }
protected get accessId() {
return this.data.access?.id;
}
protected get accessLevel(): AccessLevel { protected get accessLevel(): AccessLevel {
return this.accessForm?.get('accessLevel')?.value as AccessLevel; return this.accessForm?.get('accessLevel')?.value as AccessLevel;
} }
@ -229,10 +241,32 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return this.accessType === 'PUBLIC'; return this.accessType === 'PUBLIC';
} }
protected get mcpConfiguration() {
return {
headers: {
Authorization: `Bearer ${this.accessId}`
},
type: 'http',
url: `${this.baseUrl}${MCP_ENDPOINT}`
};
}
protected get showExpiresAtErrorMessage() { protected get showExpiresAtErrorMessage() {
return this.accessForm?.get('expiresAt')?.invalid === true; return this.accessForm?.get('expiresAt')?.invalid === true;
} }
protected get showMcpDetails() {
return this.canGrantMcpAccess && this.isMcpAccess && this.mode === 'update';
}
protected get showPublicDetails() {
return (
this.hasExperimentalFeatures &&
this.isPublicAccess &&
this.mode === 'update'
);
}
protected onCancel() { protected onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }
@ -245,6 +279,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
} }
private get isMcpAccess() {
return this.accessType === 'MCP';
}
private async createAccess() { private async createAccess() {
const filters = this.getFilters(); const filters = this.getFilters();
@ -338,7 +376,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
} }
private async updateAccess() { private async updateAccess() {
const accessId = this.data.access?.id; const accessId = this.accessId;
if (!accessId) { if (!accessId) {
return; return;

14
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -82,7 +82,7 @@
<mat-option value="READ_RESTRICTED"> <mat-option value="READ_RESTRICTED">
<gf-access-level-icon accessLevel="READ_RESTRICTED" /> <gf-access-level-icon accessLevel="READ_RESTRICTED" />
</mat-option> </mat-option>
@if (!isPublicAccess) { @if (canGrantRestrictedWriteAccess) {
<mat-option <mat-option
value="CREATE_READ_RESTRICTED_UPDATE_DELETE" value="CREATE_READ_RESTRICTED_UPDATE_DELETE"
[disabled]="!canGrantWriteAccess" [disabled]="!canGrantWriteAccess"
@ -133,6 +133,18 @@
[tags]="tags" [tags]="tags"
/> />
} }
@if (showMcpDetails || showPublicDetails) {
<h2 class="h6" i18n>Details</h2>
@if (showMcpDetails) {
<pre class="m-0"><code>{{ mcpConfiguration | json }}</code></pre>
} @else if (showPublicDetails) {
<div>
<code class="text-break"
>GET {{ baseUrl }}/api/v1/public/{{ accessId }}/portfolio</code
>
</div>
}
}
</div> </div>
<div class="justify-content-end" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onCancel()"> <button mat-button type="button" (click)="onCancel()">

84
apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts

@ -5,11 +5,17 @@ import {
} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import {
CreateAccountDto,
TransferBalanceDto,
UpdateAccountDto
} from '@ghostfolio/common/dtos';
import { AccountResponse, User } from '@ghostfolio/common/interfaces'; import { AccountResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes'; import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithPlatform } from '@ghostfolio/common/types';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { import {
@ -36,6 +42,8 @@ import {
import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component'; import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component';
import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces'; import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces';
import { TransferBalanceDialogParams } from '../transfer-balance/interfaces/interfaces';
import { GfTransferBalanceDialogComponent } from '../transfer-balance/transfer-balance-dialog.component';
import { AccountDialogMode } from './types/account-dialog-mode.type'; import { AccountDialogMode } from './types/account-dialog-mode.type';
@Component({ @Component({
@ -45,7 +53,9 @@ import { AccountDialogMode } from './types/account-dialog-mode.type';
}) })
export class GfAccountDialogHostComponent implements OnDestroy, OnInit { export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private dialogRef: MatDialogRef< private dialogRef: MatDialogRef<
GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent | GfAccountDetailDialogComponent
| GfCreateOrUpdateAccountDialogComponent
| GfTransferBalanceDialogComponent
>; >;
private readonly deviceType = computed(() => { private readonly deviceType = computed(() => {
@ -61,6 +71,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private readonly impersonationStorageService = inject( private readonly impersonationStorageService = inject(
ImpersonationStorageService ImpersonationStorageService
); );
private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -108,6 +119,24 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
return; return;
} }
if (mode === 'transferCashBalance') {
const accounts = user?.accounts ?? [];
if (
accounts.length < 2 ||
!hasPermission(user?.permissions, permissions.updateAccount) ||
this.isWriteRestricted(user, scopes.accountUpdate)
) {
this.navigateBack();
return;
}
this.openTransferBalanceDialog({ accounts });
return;
}
if (mode === 'update') { if (mode === 'update') {
if ( if (
!account || !account ||
@ -298,4 +327,55 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
}); });
}); });
} }
private openTransferBalanceDialog({
accounts
}: {
accounts: AccountWithPlatform[];
}) {
const dialogRef = this.dialog.open<
GfTransferBalanceDialogComponent,
TransferBalanceDialogParams,
TransferBalanceDto | null
>(GfTransferBalanceDialogComponent, {
data: {
accounts
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
this.dialogRef = dialogRef;
dialogRef
.afterClosed()
.pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
if (!result) {
this.navigateBack();
return;
}
this.dataService
.transferAccountBalance(result)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
error: () => {
this.notificationService.alert({
title: $localize`Oops, cash balance transfer has failed.`
});
this.navigateBack();
},
next: () => {
// Deliberately not bound to the destroy reference: navigating
// back destroys this component and the refreshed user is what
// makes the accounts page reload its data
this.userService.get(true).subscribe();
this.navigateBack();
}
});
});
}
} }

3
apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts

@ -1 +1,2 @@
export type AccountDialogMode = 'create' | 'detail' | 'update'; export type AccountDialogMode =
'create' | 'detail' | 'transferCashBalance' | 'update';

86
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -1,7 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { TransferBalanceDto } from '@ghostfolio/common/dtos';
import { AccountsResponse, User } from '@ghostfolio/common/interfaces'; import { AccountsResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
@ -9,27 +8,19 @@ import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithValue } from '@ghostfolio/common/types'; import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfFabComponent } from '@ghostfolio/ui/fab'; import { GfFabComponent } from '@ghostfolio/ui/fab';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject, inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { Router, RouterModule } from '@angular/router';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { filter, switchMap, tap } from 'rxjs';
import { DeviceDetectorService } from 'ngx-device-detector';
import { filter, of, switchMap, tap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces';
import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -54,33 +45,15 @@ export class GfAccountsPageComponent implements OnInit {
private hasImpersonationId: boolean; private hasImpersonationId: boolean;
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog);
private readonly impersonationStorageService = inject( private readonly impersonationStorageService = inject(
ImpersonationStorageService ImpersonationStorageService
); );
private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
public constructor() {
this.route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => {
if (params['transferBalanceDialog']) {
this.openTransferBalanceDialog();
}
});
}
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
@ -135,67 +108,12 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
protected onTransferBalance() {
this.router.navigate([], {
queryParams: { transferBalanceDialog: true }
});
}
private fetchAccounts() { private fetchAccounts() {
return this.dataService.fetchAccounts({ return this.dataService.fetchAccounts({
filters: this.userService.getFilters() filters: this.userService.getFilters()
}); });
} }
private openTransferBalanceDialog() {
const dialogRef = this.dialog.open<
GfTransferBalanceDialogComponent,
TransferBalanceDialogParams
>(GfTransferBalanceDialogComponent, {
data: {
accounts: this.user?.accounts
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data: any) => {
if (data) {
this.reset();
const { accountIdFrom, accountIdTo, balance }: TransferBalanceDto =
data?.account;
this.dataService
.transferAccountBalance({
accountIdFrom,
accountIdTo,
balance
})
.pipe(
catchError(() => {
this.notificationService.alert({
title: $localize`Oops, cash balance transfer has failed.`
});
return of(undefined);
}),
switchMap(() => this.fetchAccounts()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((response) => {
this.updateAccounts(response);
});
this.changeDetectorRef.markForCheck();
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private reset() { private reset() {
this.accounts = undefined; this.accounts = undefined;
this.activitiesCount = 0; this.activitiesCount = 0;

1
apps/client/src/app/pages/accounts/accounts-page.html

@ -16,7 +16,6 @@
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(transferBalance)="onTransferBalance()"
/> />
</div> </div>
</div> </div>

9
apps/client/src/app/pages/accounts/accounts-page.routes.ts

@ -6,7 +6,8 @@ import { Routes } from '@angular/router';
import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component'; import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component';
import { GfAccountsPageComponent } from './accounts-page.component'; import { GfAccountsPageComponent } from './accounts-page.component';
const { create, detail, update } = internalRoutes.accounts.subRoutes; const { create, detail, transferCashBalance, update } =
internalRoutes.accounts.subRoutes;
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -18,6 +19,12 @@ export const routes: Routes = [
path: create.path, path: create.path,
title: create.title title: create.title
}, },
{
component: GfAccountDialogHostComponent,
data: { mode: 'transferCashBalance' },
path: transferCashBalance.path,
title: transferCashBalance.title
},
{ {
children: [ children: [
{ {

4
apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts

@ -99,13 +99,13 @@ export class GfTransferBalanceDialogComponent {
} }
protected onSubmit() { protected onSubmit() {
const account: TransferBalanceDto = { const transferBalance: TransferBalanceDto = {
accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '', accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '',
accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '', accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '',
balance: Number(this.transferBalanceForm.controls.balance.value) balance: Number(this.transferBalanceForm.controls.balance.value)
}; };
this.dialogRef.close({ account }); this.dialogRef.close(transferBalance);
} }
private getAccountById(aId: string | null) { private getAccountById(aId: string | null) {

52
apps/client/src/locales/messages.it.xlf

@ -40,7 +40,7 @@
</trans-unit> </trans-unit>
<trans-unit id="9153520284278555926" datatype="html"> <trans-unit id="9153520284278555926" datatype="html">
<source>please</source> <source>please</source>
<target state="new">please</target> <target state="translated">per favore</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">333</context> <context context-type="linenumber">333</context>
@ -92,7 +92,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1351814922314683865" datatype="html"> <trans-unit id="1351814922314683865" datatype="html">
<source>with</source> <source>with</source>
<target state="new">with</target> <target state="translated">con</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">87</context>
@ -100,7 +100,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8264632476919693099" datatype="html"> <trans-unit id="8264632476919693099" datatype="html">
<source>Any person who has the link can view the data.</source> <source>Any person who has the link can view the data.</source>
<target state="new">Any person who has the link can view the data.</target> <target state="translated">Chiunque abbia il link può visualizzare i dati.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">42</context> <context context-type="linenumber">42</context>
@ -328,7 +328,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8282940047848889809" datatype="html"> <trans-unit id="8282940047848889809" datatype="html">
<source>Paid</source> <source>Paid</source>
<target state="new">Paid</target> <target state="translated">Pagamento effettuato</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">136</context>
@ -368,7 +368,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4467323362722952678" datatype="html"> <trans-unit id="4467323362722952678" datatype="html">
<source>Unknown</source> <source>Unknown</source>
<target state="new">Unknown</target> <target state="translated">Sconosciuto</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">89</context>
@ -384,7 +384,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4472298507300372923" datatype="html"> <trans-unit id="4472298507300372923" datatype="html">
<source>View and manage</source> <source>View and manage</source>
<target state="new">View and manage</target> <target state="translated">Visualizza e gestisci</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/access-level-icon/access-level-icon.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>
@ -420,7 +420,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5611965261696422586" datatype="html"> <trans-unit id="5611965261696422586" datatype="html">
<source>and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; i18n-title title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source> <source>and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; i18n-title title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="new">and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; i18n-title title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target> <target state="translated">ed è sostenuto dall’impegno dei suoi <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; i18n-title title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributori<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
@ -428,7 +428,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3060494754215793943" datatype="html"> <trans-unit id="3060494754215793943" datatype="html">
<source>Asset Profiles</source> <source>Asset Profiles</source>
<target state="translated">Profilo dell’asset</target> <target state="translated">Profili degli asset</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context>
<context context-type="linenumber">131</context> <context context-type="linenumber">131</context>
@ -492,7 +492,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3973560112150137298" datatype="html"> <trans-unit id="3973560112150137298" datatype="html">
<source>Find an account...</source> <source>Find an account...</source>
<target state="new">Find an account...</target> <target state="translated">Trova un account...</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/assistant/assistant.component.ts</context> <context context-type="sourcefile">libs/ui/src/lib/assistant/assistant.component.ts</context>
<context context-type="linenumber">448</context> <context context-type="linenumber">448</context>
@ -528,7 +528,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8410000928786197012" datatype="html"> <trans-unit id="8410000928786197012" datatype="html">
<source>Watch the Ghostfol.io Trailer on YouTube</source> <source>Watch the Ghostfol.io Trailer on YouTube</source>
<target state="new">Watch the Ghostfol.io Trailer on YouTube</target> <target state="translated">Guarda il trailer di Ghostfol.io su YouTube</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">19</context>
@ -644,7 +644,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6249687295240367729" datatype="html"> <trans-unit id="6249687295240367729" datatype="html">
<source>Explore our guides to help you get started with investing and managing your finances.</source> <source>Explore our guides to help you get started with investing and managing your finances.</source>
<target state="new">Explore our guides to help you get started with investing and managing your finances.</target> <target state="translated">Esplora le nostre guide per iniziare a investire e a gestire le tue finanze.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
@ -668,7 +668,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8155924822307560615" datatype="html"> <trans-unit id="8155924822307560615" datatype="html">
<source>Update Account</source> <source>Update Account</source>
<target state="new">Update Account</target> <target state="translated">Aggiorna account</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
<context context-type="linenumber">101</context> <context context-type="linenumber">101</context>
@ -740,7 +740,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2395205455607568422" datatype="html"> <trans-unit id="2395205455607568422" datatype="html">
<source>No auto-renewal on membership.</source> <source>No auto-renewal on membership.</source>
<target state="new">No auto-renewal on membership.</target> <target state="translated">Nessun rinnovo automatico dell’abbonamento.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-membership/user-account-membership.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-membership/user-account-membership.html</context>
<context context-type="linenumber">77</context> <context context-type="linenumber">77</context>
@ -820,7 +820,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2047393478951255414" datatype="html"> <trans-unit id="2047393478951255414" datatype="html">
<source>Creation</source> <source>Creation</source>
<target state="new">Creation</target> <target state="translated">Creazione</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">191</context>
@ -876,7 +876,7 @@
</trans-unit> </trans-unit>
<trans-unit id="504959340312049745" datatype="html"> <trans-unit id="504959340312049745" datatype="html">
<source>Split Ratio</source> <source>Split Ratio</source>
<target state="new">Split Ratio</target> <target state="translated">Rapporto di frazionamento</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">532</context> <context context-type="linenumber">532</context>
@ -884,7 +884,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1022809987579838138" datatype="html"> <trans-unit id="1022809987579838138" datatype="html">
<source>Last Used</source> <source>Last Used</source>
<target state="new">Last Used</target> <target state="translated">Ultimo utilizzo</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
@ -892,7 +892,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1029620928611278483" datatype="html"> <trans-unit id="1029620928611278483" datatype="html">
<source>An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source> <source>An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</source>
<target state="new">An error occurred while merging this asset profile into <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target> <target state="translated">Si è verificato un errore durante l’unione di questo profilo di asset in <x id="PH" equiv-text="symbol"/> (<x id="PH_1" equiv-text="dataSource"/>).</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">942</context> <context context-type="linenumber">942</context>
@ -936,7 +936,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6623155069376115284" datatype="html"> <trans-unit id="6623155069376115284" datatype="html">
<source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</source> <source>Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</source>
<target state="new">Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section.</target> <target state="translated">Trova risposte rapide alle domande più frequenti su Ghostfolio nella nostra sezione FAQ.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/resources/overview/resources-overview.component.ts</context>
<context context-type="linenumber">16</context> <context context-type="linenumber">16</context>
@ -992,7 +992,7 @@
</trans-unit> </trans-unit>
<trans-unit id="273949409065292025" datatype="html"> <trans-unit id="273949409065292025" datatype="html">
<source>Energy</source> <source>Energy</source>
<target state="new">Energy</target> <target state="translated">Energia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">96</context>
@ -1220,7 +1220,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8793726805339626615" datatype="html"> <trans-unit id="8793726805339626615" datatype="html">
<source>Ghostfolio in Numbers: Monthly Active Users (MAU)</source> <source>Ghostfolio in Numbers: Monthly Active Users (MAU)</source>
<target state="new">Ghostfolio in Numbers: Monthly Active Users (MAU)</target> <target state="translated">Ghostfolio in cifre: utenti attivi mensili (MAU)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.html</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">63</context>
@ -1240,7 +1240,7 @@
</trans-unit> </trans-unit>
<trans-unit id="366169681580494481" datatype="html"> <trans-unit id="366169681580494481" datatype="html">
<source>Performance with currency effect</source> <source>Performance with currency effect</source>
<target state="new">Performance with currency effect</target> <target state="translated">Performance con effetto valuta</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">125</context>
@ -1260,7 +1260,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3676123092874008298" datatype="html"> <trans-unit id="3676123092874008298" datatype="html">
<source>Identifier has been copied to the clipboard</source> <source>Identifier has been copied to the clipboard</source>
<target state="new">Identifier has been copied to the clipboard</target> <target state="translated">Identificativo copiato negli appunti</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/access-table/access-table.component.ts</context>
<context context-type="linenumber">149</context> <context context-type="linenumber">149</context>
@ -1340,7 +1340,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1259421956660976189" datatype="html"> <trans-unit id="1259421956660976189" datatype="html">
<source>Add Account</source> <source>Add Account</source>
<target state="new">Add Account</target> <target state="translated">Aggiungi account</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">87</context>
@ -1356,7 +1356,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4432485267082955422" datatype="html"> <trans-unit id="4432485267082955422" datatype="html">
<source>Consumer Defensive</source> <source>Consumer Defensive</source>
<target state="new">Consumer Defensive</target> <target state="translated">Beni di consumo difensivi</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">95</context> <context context-type="linenumber">95</context>
@ -1408,7 +1408,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3848479214898655176" datatype="html"> <trans-unit id="3848479214898655176" datatype="html">
<source>Utilities</source> <source>Utilities</source>
<target state="new">Utilities</target> <target state="translated">Servizi di pubblica utilità</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">103</context>
@ -9414,7 +9414,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1184917049575486230" datatype="html"> <trans-unit id="1184917049575486230" datatype="html">
<source>Support Ghostfolio</source> <source>Support Ghostfolio</source>
<target state="new">Support Ghostfolio</target> <target state="translated">Supporta Ghostfolio</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">170</context> <context context-type="linenumber">170</context>

20
libs/common/src/lib/personal-finance-tools.ts

@ -1590,6 +1590,26 @@ export const personalFinanceTools: Product[] = [
slogan: 'The Intelligent Portfolio Tracker', slogan: 'The Intelligent Portfolio Tracker',
url: 'https://www.navexa.com' url: 'https://www.navexa.com'
}, },
{
categories: [
'BUDGETING',
'CRYPTOCURRENCY',
'ETF_TRACKING',
'FINANCIAL_PLANNING',
'NET_WORTH_TRACKING',
'STOCK_TRACKING'
],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'neontra',
languages: ['English'],
name: 'Neontra',
origin: 'CA',
platforms: ['ANDROID', 'IOS', 'WEB'],
pricingPerYear: '$75',
slogan: 'Everything you own. Everything you owe. Everything in one place.',
url: 'https://neontra.com'
},
{ {
categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'], categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
hasFreePlan: true, hasFreePlan: true,

5
libs/common/src/lib/routes/routes.ts

@ -93,6 +93,11 @@ export const internalRoutes = {
}, },
title: $localize`Account` title: $localize`Account`
}, },
transferCashBalance: {
path: 'transfer-cash-balance',
routerLink: ['/accounts', 'transfer-cash-balance'],
title: $localize`Transfer Cash Balance`
},
update: { update: {
path: 'update', path: 'update',
routerLink: (aAccountId: string) => { routerLink: (aAccountId: string) => {

21
libs/common/src/lib/scopes.ts

@ -64,7 +64,9 @@ export const SCOPES_OF_READ_RESTRICTED_ACCESS: readonly Scope[] =
/** /**
* Maximum scopes per access type. The scopes stored on an access are * Maximum scopes per access type. The scopes stored on an access are
* intersected with it, hence a scope which the type does not permit stays * intersected with it, hence a scope which the type does not permit stays
* ineffective even if it is stored. * ineffective even if it is stored. A type which cannot grant the restricted
* write access drops the write scopes in addition, unless the access reads the
* monetary values.
*/ */
const SCOPES_OF_TYPE: Record<AccessType, readonly Scope[]> = { const SCOPES_OF_TYPE: Record<AccessType, readonly Scope[]> = {
MCP: [...SCOPES_OF_READ_RESTRICTED_ACCESS, scopes.activityCreate], MCP: [...SCOPES_OF_READ_RESTRICTED_ACCESS, scopes.activityCreate],
@ -72,6 +74,14 @@ const SCOPES_OF_TYPE: Record<AccessType, readonly Scope[]> = {
PUBLIC: SCOPES_OF_PUBLIC_ACCESS PUBLIC: SCOPES_OF_PUBLIC_ACCESS
}; };
/**
* Access types which combine a write scope with the restricted read access,
* because their tools change data without exposing the monetary values
*/
export function canGrantRestrictedWriteAccess({ type }: { type: AccessType }) {
return type === 'MCP';
}
/** /**
* Access level which the scopes of an access grant * Access level which the scopes of an access grant
*/ */
@ -96,9 +106,16 @@ export function getScopesOfAccess({
}): string[] { }): string[] {
const scopesToEvaluate = scopesOfAccess ?? []; const scopesToEvaluate = scopesOfAccess ?? [];
const permitsWriteAccess =
canGrantRestrictedWriteAccess({ type }) ||
hasScope(scopesToEvaluate, scopes.portfolioReadValues);
// An unknown scope is dropped // An unknown scope is dropped
return SCOPES_OF_TYPE[type].filter((scope) => { return SCOPES_OF_TYPE[type].filter((scope) => {
return scopesToEvaluate.includes(scope); return (
scopesToEvaluate.includes(scope) &&
(permitsWriteAccess || !SCOPES_OF_WRITE_ACCESS.includes(scope))
);
}); });
} }

2
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -4,7 +4,7 @@
class="align-items-center d-flex" class="align-items-center d-flex"
mat-stroked-button mat-stroked-button
[disabled]="dataSource?.data.length < 2" [disabled]="dataSource?.data.length < 2"
(click)="onTransferBalance()" [routerLink]="transferCashBalanceRouterLink"
> >
<ion-icon class="mr-2" name="arrow-redo-outline" /> <ion-icon class="mr-2" name="arrow-redo-outline" />
<span><ng-container i18n>Transfer Cash Balance</ng-container>...</span> <span><ng-container i18n>Transfer Cash Balance</ng-container>...</span>

8
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -75,7 +75,6 @@ export class GfAccountsTableComponent {
public readonly totalValueInBaseCurrency = input<number>(); public readonly totalValueInBaseCurrency = input<number>();
public readonly accountDeleted = output<string>(); public readonly accountDeleted = output<string>();
public readonly transferBalance = output<void>();
public readonly sort = viewChild.required(MatSort); public readonly sort = viewChild.required(MatSort);
@ -141,6 +140,9 @@ export class GfAccountsTableComponent {
protected readonly isLoading = computed(() => !this.accounts()); protected readonly isLoading = computed(() => !this.accounts());
protected readonly transferCashBalanceRouterLink =
internalRoutes.accounts.subRoutes.transferCashBalance.routerLink;
private readonly notificationService = inject(NotificationService); private readonly notificationService = inject(NotificationService);
private readonly router = inject(Router); private readonly router = inject(Router);
@ -197,8 +199,4 @@ export class GfAccountsTableComponent {
title: aComment title: aComment
}); });
} }
protected onTransferBalance() {
this.transferBalance.emit();
}
} }

4
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -50,7 +50,7 @@ export class GfHoldingsTableComponent {
public readonly hasPermissionToOpenDetails = input(true); public readonly hasPermissionToOpenDetails = input(true);
public readonly hasPermissionToShowQuantities = input(true); public readonly hasPermissionToShowQuantities = input(true);
public readonly hasPermissionToShowValues = input(true); public readonly hasPermissionToShowValues = input(true);
public readonly holdings = input.required<PortfolioPosition[]>(); public readonly holdings = input.required<PortfolioPosition[] | undefined>();
public readonly locale = input(getLocale()); public readonly locale = input(getLocale());
public readonly pageSize = model(Number.MAX_SAFE_INTEGER); public readonly pageSize = model(Number.MAX_SAFE_INTEGER);
@ -89,7 +89,7 @@ export class GfHoldingsTableComponent {
// Reactive data update // Reactive data update
effect(() => { effect(() => {
this.dataSource.data = this.holdings(); this.dataSource.data = this.holdings() ?? [];
}); });
// Reactive view connection // Reactive view connection

120
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.65.0", "version": "3.66.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.65.0", "version": "3.66.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -47,9 +47,9 @@
"@nestjs/serve-static": "5.0.5", "@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0", "@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0", "@openrouter/ai-sdk-provider": "3.0.0",
"@prisma/adapter-pg": "7.9.1", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.9.1", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.0", "@rekog/mcp-nest": "2.0.2",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "13.3.1",
"ai": "7.0.37", "ai": "7.0.37",
@ -134,7 +134,7 @@
"@nx/storybook": "23.1.1", "@nx/storybook": "23.1.1",
"@nx/web": "23.1.1", "@nx/web": "23.1.1",
"@nx/workspace": "23.1.1", "@nx/workspace": "23.1.1",
"@prisma/config": "7.9.1", "@prisma/config": "7.10.0",
"@schematics/angular": "22.1.4", "@schematics/angular": "22.1.4",
"@storybook/addon-docs": "10.5.7", "@storybook/addon-docs": "10.5.7",
"@storybook/addon-themes": "10.5.7", "@storybook/addon-themes": "10.5.7",
@ -165,7 +165,7 @@
"nx": "23.1.1", "nx": "23.1.1",
"prettier": "3.9.6", "prettier": "3.9.6",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.9.1", "prisma": "7.10.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "9.0.0", "replace-in-file": "9.0.0",
@ -13186,24 +13186,24 @@
} }
}, },
"node_modules/@prisma/adapter-pg": { "node_modules/@prisma/adapter-pg": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz",
"integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==", "integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/driver-adapter-utils": "7.9.1", "@prisma/driver-adapter-utils": "7.10.0",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"postgres-array": "3.0.4" "postgres-array": "3.0.4"
} }
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz",
"integrity": "sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==", "integrity": "sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/client-runtime-utils": "7.9.1" "@prisma/client-runtime-utils": "7.10.0"
}, },
"engines": { "engines": {
"node": "^20.19 || ^22.12 || >=24.0" "node": "^20.19 || ^22.12 || >=24.0"
@ -13222,15 +13222,15 @@
} }
}, },
"node_modules/@prisma/client-runtime-utils": { "node_modules/@prisma/client-runtime-utils": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.0.tgz",
"integrity": "sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==", "integrity": "sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/config": { "node_modules/@prisma/config": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz",
"integrity": "sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==", "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -13241,9 +13241,9 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz",
"integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==", "integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/dev": { "node_modules/@prisma/dev": {
@ -13271,65 +13271,65 @@
} }
}, },
"node_modules/@prisma/driver-adapter-utils": { "node_modules/@prisma/driver-adapter-utils": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz",
"integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", "integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.9.1" "@prisma/debug": "7.10.0"
} }
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz",
"integrity": "sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==", "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.9.1", "@prisma/debug": "7.10.0",
"@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3",
"@prisma/fetch-engine": "7.9.1", "@prisma/fetch-engine": "7.10.0",
"@prisma/get-platform": "7.9.1" "@prisma/get-platform": "7.10.0"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz",
"integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==", "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz",
"integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.9.1" "@prisma/debug": "7.10.0"
} }
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz",
"integrity": "sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==", "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.9.1", "@prisma/debug": "7.10.0",
"@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3",
"@prisma/get-platform": "7.9.1" "@prisma/get-platform": "7.10.0"
} }
}, },
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz",
"integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "7.9.1" "@prisma/debug": "7.10.0"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
@ -13587,9 +13587,9 @@
} }
}, },
"node_modules/@rekog/mcp-nest": { "node_modules/@rekog/mcp-nest": {
"version": "2.0.0", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-2.0.2.tgz",
"integrity": "sha512-lT5V26V4fvPay50zMiw2KrEqlDSuzdAeyoQFd0+vfAwb40G6H8p8psTzgq5atTgNHktRPI7u4x8lrm8X844Asg==", "integrity": "sha512-kuye6wmRk5Gw4KZSCSXd83cWznRw4eFbvNUXsd8+1dYcS4uWKVVbRvxQIKNCyxc2UcljpsmLpnksr1NCRn3YAQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"multer": "^2.2.0", "multer": "^2.2.0",
@ -30547,16 +30547,16 @@
} }
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "7.9.1", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz",
"integrity": "sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==", "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/config": "7.9.1", "@prisma/config": "7.10.0",
"@prisma/dev": "0.24.17", "@prisma/dev": "0.24.17",
"@prisma/engines": "7.9.1", "@prisma/engines": "7.10.0",
"@prisma/studio-core": "0.33.0", "@prisma/studio-core": "0.33.0",
"mysql2": "3.15.3", "mysql2": "3.15.3",
"postgres": "3.4.7" "postgres": "3.4.7"

12
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.65.0", "version": "3.66.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -91,9 +91,9 @@
"@nestjs/serve-static": "5.0.5", "@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0", "@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0", "@openrouter/ai-sdk-provider": "3.0.0",
"@prisma/adapter-pg": "7.9.1", "@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.9.1", "@prisma/client": "7.10.0",
"@rekog/mcp-nest": "2.0.0", "@rekog/mcp-nest": "2.0.2",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "13.3.1",
"ai": "7.0.37", "ai": "7.0.37",
@ -178,7 +178,7 @@
"@nx/storybook": "23.1.1", "@nx/storybook": "23.1.1",
"@nx/web": "23.1.1", "@nx/web": "23.1.1",
"@nx/workspace": "23.1.1", "@nx/workspace": "23.1.1",
"@prisma/config": "7.9.1", "@prisma/config": "7.10.0",
"@schematics/angular": "22.1.4", "@schematics/angular": "22.1.4",
"@storybook/addon-docs": "10.5.7", "@storybook/addon-docs": "10.5.7",
"@storybook/addon-themes": "10.5.7", "@storybook/addon-themes": "10.5.7",
@ -209,7 +209,7 @@
"nx": "23.1.1", "nx": "23.1.1",
"prettier": "3.9.6", "prettier": "3.9.6",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.9.1", "prisma": "7.10.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "9.0.0", "replace-in-file": "9.0.0",

Loading…
Cancel
Save