Browse Source

Merge branch 'main' into task/improve-instructions-of-mcp-server

pull/8263/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
a16936ae91
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/build-code.yml
  2. 17
      CHANGELOG.md
  3. 2
      DEVELOPMENT.md
  4. 16
      apps/api/src/app/__snapshots__/app.module.spec.ts.snap
  5. 2
      apps/api/src/app/activities/activities.module.ts
  6. 16
      apps/api/src/app/app.module.ts
  7. 17
      apps/api/src/app/asset/asset.module.ts
  8. 19
      apps/api/src/app/auth-device/auth-device.module.ts
  9. 4
      apps/api/src/app/auth/auth.module.ts
  10. 2
      apps/api/src/app/auth/web-auth.service.ts
  11. 3
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  12. 3
      apps/api/src/app/endpoints/asset-profiles/asset.controller.ts
  13. 10
      apps/api/src/app/endpoints/auth-devices/auth-devices.controller.ts
  14. 11
      apps/api/src/app/endpoints/auth-devices/auth-devices.module.ts
  15. 0
      apps/api/src/app/endpoints/cache/cache.controller.ts
  16. 0
      apps/api/src/app/endpoints/cache/cache.module.ts
  17. 18
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.controller.ts
  18. 13
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.module.ts
  19. 2
      apps/api/src/app/endpoints/exchange-rates/exchange-rates.service.ts
  20. 0
      apps/api/src/app/endpoints/info/info.controller.ts
  21. 0
      apps/api/src/app/endpoints/info/info.module.ts
  22. 0
      apps/api/src/app/endpoints/info/info.service.ts
  23. 0
      apps/api/src/app/endpoints/logo/get-logo.dto.ts
  24. 0
      apps/api/src/app/endpoints/logo/logo.controller.ts
  25. 0
      apps/api/src/app/endpoints/logo/logo.module.ts
  26. 0
      apps/api/src/app/endpoints/logo/logo.service.ts
  27. 14
      apps/api/src/app/exchange-rate/exchange-rate.module.ts
  28. 2
      apps/api/src/app/import/import.module.ts
  29. 12
      apps/api/src/services/auth-device/auth-device.module.ts
  30. 0
      apps/api/src/services/auth-device/auth-device.service.ts
  31. 12
      apps/client/src/app/components/home-overview/home-overview.component.ts
  32. 14
      apps/client/src/app/components/home-overview/home-overview.html
  33. 13
      apps/client/src/app/components/home-overview/home-overview.scss
  34. 18
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  35. 42
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  36. 10
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  37. 5
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  38. 2
      apps/client/src/app/services/web-authn.service.ts
  39. 14
      libs/common/src/lib/config.ts
  40. 2
      libs/common/src/lib/interfaces/index.ts
  41. 3
      libs/common/src/lib/interfaces/responses/exchange-rate-response.interface.ts
  42. 7
      libs/ui/src/lib/dialog-footer/dialog-footer.component.html
  43. 4
      libs/ui/src/lib/dialog-footer/dialog-footer.component.scss
  44. 4
      libs/ui/src/lib/dialog-footer/dialog-footer.component.ts
  45. 13
      libs/ui/src/lib/dialog-header/dialog-header.component.html
  46. 11
      libs/ui/src/lib/dialog-header/dialog-header.component.scss
  47. 4
      libs/ui/src/lib/dialog-header/dialog-header.component.ts
  48. 13
      libs/ui/src/lib/services/data.service.ts
  49. 30
      package-lock.json
  50. 10
      package.json

2
.github/workflows/build-code.yml

@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
node_version:
- 22.23.2
- 22.23.4
steps:
- name: Checkout code
uses: actions/checkout@v4

17
CHANGELOG.md

@ -7,12 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Added a link to the holdings tab in the holdings table on the overview tab of the home page (experimental)
### Changed
- Improved the style of the close button in the dialog footer component
- Improved the style of the close button in the dialog header component
- Improved the instructions of the server of the Model Context Protocol (MCP) to give the limits of the Basic subscription only to a user with the Basic subscription (experimental)
- Upgraded `prettier` from version `3.9.9` to `3.9.10`
## 3.90.0 - 2026-10-21
### Changed
- Extended the variations of the interstitials for the subscription by a trial
- Deprecated the `api/v1/auth-device` endpoint in favor of the `api/v1/auth-devices` endpoint
- Deprecated the `api/v1/exchange-rate` endpoint in favor of the `api/v1/exchange-rates` endpoint
- Upgraded the `Node.js` engine from version `>=22.22.3` to `>=22.23.4` (`package.json`)
- Upgraded `@modelcontextprotocol/core` and `@modelcontextprotocol/server` from version `2.0.0` to `2.3.1`
- Upgraded `@modelcontextprotocol/node` from version `2.0.0` to `2.1.1`
- Upgraded `@rekog/mcp-nest` from version `2.0.7` to `2.0.8`
- Upgraded `undici` from version `8.11.2` to `8.12.2`
## 3.89.0 - 2026-10-20

2
DEVELOPMENT.md

@ -5,7 +5,7 @@
### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version `>=22.18.0`)
- [Node.js](https://nodejs.org/en/download) (version `>=22.23.4`)
- Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)

16
apps/api/src/app/__snapshots__/app.module.spec.ts.snap

@ -116,6 +116,15 @@ exports[`AppModule should keep the security of each route 1`] = `
"interceptors": [],
"permission": "deleteAuthDevice",
},
"DELETE /auth-devices/:id": {
"allowDuringImpersonation": true,
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
"permission": "deleteAuthDevice",
},
"DELETE /benchmarks/:dataSource/:symbol": {
"allowDuringImpersonation": true,
"guards": [
@ -489,6 +498,13 @@ exports[`AppModule should keep the security of each route 1`] = `
],
"interceptors": [],
},
"GET /exchange-rates/:symbol/:dateString": {
"guards": [
"AuthGuard('jwt')",
"HasPermissionGuard",
],
"interceptors": [],
},
"GET /export": {
"guards": [
"AuthGuard('jwt')",

2
apps/api/src/app/activities/activities.module.ts

@ -1,6 +1,5 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -29,7 +28,6 @@ import { ActivitiesService } from './activities.service';
ApiModule,
AssetProfileSplitModule,
BenchmarkModule,
CacheModule,
DataGatheringQueueModule,
DataProviderModule,
ExchangeRateDataModule,

16
apps/api/src/app/app.module.ts

@ -40,16 +40,18 @@ import { AccountModule } from './account/account.module';
import { ActivitiesModule } from './activities/activities.module';
import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module';
import { AssetsModule } from './endpoints/assets/assets.module';
import { AuthDevicesModule } from './endpoints/auth-devices/auth-devices.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { CacheModule } from './endpoints/cache/cache.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { ExchangeRatesModule } from './endpoints/exchange-rates/exchange-rates.module';
import { InfoModule } from './endpoints/info/info.module';
import { LogoModule } from './endpoints/logo/logo.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { McpModule } from './endpoints/mcp/mcp.module';
import { PlatformsModule } from './endpoints/platforms/platforms.module';
@ -57,12 +59,9 @@ import { PublicModule } from './endpoints/public/public.module';
import { SitemapModule } from './endpoints/sitemap/sitemap.module';
import { TagsModule } from './endpoints/tags/tags.module';
import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
import { ExchangeRateModule } from './exchange-rate/exchange-rate.module';
import { ExportModule } from './export/export.module';
import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module';
import { PortfolioModule } from './portfolio/portfolio.module';
import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module';
@ -78,9 +77,8 @@ import { UserModule } from './user/user.module';
AiModule,
ApiKeysModule,
AssetProfilesModule,
AssetModule,
AssetsModule,
AuthDeviceModule,
AuthDevicesModule,
AuthModule,
BenchmarksModule,
BullBoardModule.forRoot({
@ -119,8 +117,8 @@ import { UserModule } from './user/user.module';
DataProviderModule,
EventEmitterModule.forRoot(),
EventsModule,
ExchangeRateModule,
ExchangeRateDataModule,
ExchangeRatesModule,
ExportModule,
GhostfolioModule,
HealthModule,

17
apps/api/src/app/asset/asset.module.ts

@ -1,17 +0,0 @@
import { AssetProfilesModule } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { Module } from '@nestjs/common';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetController],
imports: [
AssetProfilesModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
})
export class AssetModule {}

19
apps/api/src/app/auth-device/auth-device.module.ts

@ -1,19 +0,0 @@
import { AuthDeviceController } from '@ghostfolio/api/app/auth-device/auth-device.controller';
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
controllers: [AuthDeviceController],
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }
}),
PrismaModule
],
providers: [AuthDeviceService]
})
export class AuthDeviceModule {}

4
apps/api/src/app/auth/auth.module.ts

@ -1,9 +1,9 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { AuthDeviceModule } from '@ghostfolio/api/services/auth-device/auth-device.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
@ -29,6 +29,7 @@ import { OidcStrategy } from './oidc.strategy';
controllers: [AuthController],
imports: [
ApiModule,
AuthDeviceModule,
ConfigurationModule,
FetchModule,
JwtModule.register({
@ -45,7 +46,6 @@ import { OidcStrategy } from './oidc.strategy';
providers: [
ApiKeyService,
ApiKeyStrategy,
AuthDeviceService,
AuthService,
GoogleStrategy,
JwtStrategy,

2
apps/api/src/app/auth/web-auth.service.ts

@ -1,7 +1,7 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { AuthDeviceService } from '@ghostfolio/api/services/auth-device/auth-device.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { RedisCacheService } from '@ghostfolio/api/services/redis-cache/redis-cache.service';

3
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -15,9 +15,10 @@ import { Module } from '@nestjs/common';
import { AssetProfilesController } from './asset-profiles.controller';
import { AssetProfilesService } from './asset-profiles.service';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetProfilesController],
controllers: [AssetController, AssetProfilesController],
exports: [AssetProfilesService],
imports: [
ActivitiesModule,

3
apps/api/src/app/asset/asset.controller.ts → apps/api/src/app/endpoints/asset-profiles/asset.controller.ts

@ -1,4 +1,3 @@
import { AssetProfilesService } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AssetResponse } from '@ghostfolio/common/interfaces';
@ -7,6 +6,8 @@ import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { pick } from 'lodash-es';
import { AssetProfilesService } from './asset-profiles.service';
@Controller('asset')
export class AssetController {
public constructor(

10
apps/api/src/app/auth-device/auth-device.controller.ts → apps/api/src/app/endpoints/auth-devices/auth-devices.controller.ts

@ -1,7 +1,7 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { AllowDuringImpersonation } from '@ghostfolio/api/decorators/allow-during-impersonation.decorator';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { AuthDeviceService } from '@ghostfolio/api/services/auth-device/auth-device.service';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -18,8 +18,12 @@ import { AuthGuard } from '@nestjs/passport';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@AllowDuringImpersonation()
@Controller('auth-device')
export class AuthDeviceController {
@Controller([
'auth-devices',
/** @deprecated */
'auth-device'
])
export class AuthDevicesController {
public constructor(
private readonly authDeviceService: AuthDeviceService,
@Inject(REQUEST) private readonly request: RequestWithUser

11
apps/api/src/app/endpoints/auth-devices/auth-devices.module.ts

@ -0,0 +1,11 @@
import { AuthDeviceModule } from '@ghostfolio/api/services/auth-device/auth-device.module';
import { Module } from '@nestjs/common';
import { AuthDevicesController } from './auth-devices.controller';
@Module({
controllers: [AuthDevicesController],
imports: [AuthDeviceModule]
})
export class AuthDevicesModule {}

0
apps/api/src/app/cache/cache.controller.ts → apps/api/src/app/endpoints/cache/cache.controller.ts

0
apps/api/src/app/cache/cache.module.ts → apps/api/src/app/endpoints/cache/cache.module.ts

18
apps/api/src/app/exchange-rate/exchange-rate.controller.ts → apps/api/src/app/endpoints/exchange-rates/exchange-rates.controller.ts

@ -1,5 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { DataProviderHistoricalResponse } from '@ghostfolio/common/interfaces';
import { ExchangeRateResponse } from '@ghostfolio/common/interfaces';
import {
Controller,
@ -12,12 +12,16 @@ import { AuthGuard } from '@nestjs/passport';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ExchangeRateService } from './exchange-rate.service';
import { ExchangeRatesService } from './exchange-rates.service';
@Controller('exchange-rate')
export class ExchangeRateController {
@Controller([
'exchange-rates',
/** @deprecated */
'exchange-rate'
])
export class ExchangeRatesController {
public constructor(
private readonly exchangeRateService: ExchangeRateService
private readonly exchangeRatesService: ExchangeRatesService
) {}
@Get(':symbol/:dateString')
@ -25,10 +29,10 @@ export class ExchangeRateController {
public async getExchangeRate(
@Param('dateString') dateString: string,
@Param('symbol') symbol: string
): Promise<DataProviderHistoricalResponse> {
): Promise<ExchangeRateResponse> {
const date = parseISO(dateString);
const exchangeRate = await this.exchangeRateService.getExchangeRate({
const exchangeRate = await this.exchangeRatesService.getExchangeRate({
date,
symbol
});

13
apps/api/src/app/endpoints/exchange-rates/exchange-rates.module.ts

@ -0,0 +1,13 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { Module } from '@nestjs/common';
import { ExchangeRatesController } from './exchange-rates.controller';
import { ExchangeRatesService } from './exchange-rates.service';
@Module({
controllers: [ExchangeRatesController],
imports: [ExchangeRateDataModule],
providers: [ExchangeRatesService]
})
export class ExchangeRatesModule {}

2
apps/api/src/app/exchange-rate/exchange-rate.service.ts → apps/api/src/app/endpoints/exchange-rates/exchange-rates.service.ts

@ -3,7 +3,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExchangeRateService {
export class ExchangeRatesService {
public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService
) {}

0
apps/api/src/app/info/info.controller.ts → apps/api/src/app/endpoints/info/info.controller.ts

0
apps/api/src/app/info/info.module.ts → apps/api/src/app/endpoints/info/info.module.ts

0
apps/api/src/app/info/info.service.ts → apps/api/src/app/endpoints/info/info.service.ts

0
apps/api/src/app/logo/get-logo.dto.ts → apps/api/src/app/endpoints/logo/get-logo.dto.ts

0
apps/api/src/app/logo/logo.controller.ts → apps/api/src/app/endpoints/logo/logo.controller.ts

0
apps/api/src/app/logo/logo.module.ts → apps/api/src/app/endpoints/logo/logo.module.ts

0
apps/api/src/app/logo/logo.service.ts → apps/api/src/app/endpoints/logo/logo.service.ts

14
apps/api/src/app/exchange-rate/exchange-rate.module.ts

@ -1,14 +0,0 @@
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { Module } from '@nestjs/common';
import { ExchangeRateController } from './exchange-rate.controller';
import { ExchangeRateService } from './exchange-rate.service';
@Module({
controllers: [ExchangeRateController],
exports: [ExchangeRateService],
imports: [ExchangeRateDataModule],
providers: [ExchangeRateService]
})
export class ExchangeRateModule {}

2
apps/api/src/app/import/import.module.ts

@ -1,6 +1,5 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -28,7 +27,6 @@ import { ImportService } from './import.service';
AccountModule,
ActivitiesModule,
ApiModule,
CacheModule,
ConfigurationModule,
DataGatheringQueueModule,
DataProviderModule,

12
apps/api/src/services/auth-device/auth-device.module.ts

@ -0,0 +1,12 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AuthDeviceService } from './auth-device.service';
@Module({
exports: [AuthDeviceService],
imports: [PrismaModule],
providers: [AuthDeviceService]
})
export class AuthDeviceModule {}

0
apps/api/src/app/auth-device/auth-device.service.ts → apps/api/src/services/auth-device/auth-device.service.ts

12
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -36,6 +36,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
@ -44,6 +47,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
GfHoldingsTableComponent,
GfLineChartComponent,
GfPortfolioPerformanceComponent,
IonIcon,
MatButtonModule,
MatCardModule,
RouterModule
@ -71,9 +75,15 @@ export class GfHomeOverviewComponent implements OnInit {
protected readonly user = signal<User | null>(null);
protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink;
protected readonly routerLinkHomeHoldings =
internalRoutes.home.subRoutes.holdings.routerLink;
protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink;
protected readonly routerLinkPortfolioActivities =
internalRoutes.portfolio.subRoutes.activities.routerLink;
protected readonly routerLinkPortfolioActivitiesCreate =
internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink;
@ -134,6 +144,8 @@ export class GfHomeOverviewComponent implements OnInit {
this.update();
}
});
addIcons({ arrowForwardOutline });
}
public ngOnInit() {

14
apps/client/src/app/components/home-overview/home-overview.html

@ -101,8 +101,20 @@
appearance="outlined"
class="h-100 holdings position-absolute w-100"
>
<mat-card-header>
<mat-card-header class="align-items-center">
<mat-card-title i18n>Holdings</mat-card-title>
<a
class="ms-auto"
i18n-title
mat-icon-button
title="View Details"
[routerLink]="routerLinkHomeHoldings"
>
<ion-icon
class="d-block text-body-secondary"
name="arrow-forward-outline"
/>
</a>
</mat-card-header>
<mat-card-content class="overflow-auto">
<gf-holdings-table

13
apps/client/src/app/components/home-overview/home-overview.scss

@ -34,6 +34,19 @@
&.holdings {
--mat-card-title-text-size: var(--mat-sys-title-medium-size);
mat-card-header {
a[mat-icon-button] {
--mat-icon-button-icon-size: 1rem;
margin-right: -0.125rem;
margin-top: -0.5rem;
}
mat-card-title {
padding-left: 0.1875rem;
}
}
mat-card-content {
scrollbar-width: none;

18
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts

@ -1,3 +1,5 @@
import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -51,6 +53,7 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
public remainingSkipButtonDelay =
GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = publicRoutes.pricing.routerLink;
public trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
public variantIndex: number;
public constructor(
@ -59,9 +62,18 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent>
) {
this.variantIndex = Math.floor(
Math.random() * GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT
);
const isEligibleForTrial =
!this.data.user?.subscription?.subscribedAt &&
hasPermission(
this.data.user?.permissions,
permissions.updateUserSettings
);
const variantsCount = isEligibleForTrial
? GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT + 1
: GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT;
this.variantIndex = Math.floor(Math.random() * variantsCount);
addIcons({ arrowForwardOutline, checkmarkCircleOutline });
}

42
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

@ -195,4 +195,46 @@
</a>
</div>
}
@case (4) {
<div class="flex-grow-1 pt-0" mat-dialog-content>
<div class="intro-container">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
</div>
</div>
<div>
<h5 class="align-items-center d-flex justify-content-center mb-3">
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ms-1" [enableLink]="false" />
</h5>
<div class="fw-normal h5 mb-3 text-center" i18n>
Request a free trial
</div>
<p class="m-0 text-center" i18n>
No credit card required. The trial ends automatically.
</p>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button
mat-button
[disabled]="remainingSkipButtonDelay > 0"
(click)="closeDialog()"
>
<ng-container i18n>Skip</ng-container>
@if (remainingSkipButtonDelay > 0) {
({{ remainingSkipButtonDelay }})
}
</button>
<a
color="primary"
mat-flat-button
[href]="trySubscriptionMailHref"
(click)="closeDialog()"
>
<span i18n>Try Premium</span>
<ion-icon class="ms-1" name="arrow-forward-outline" />
</a>
</div>
}
}

10
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -1,5 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { E_MAIL_LINE_BREAK } from '@ghostfolio/common/config';
import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
@ -49,13 +49,7 @@ export class GfUserAccountMembershipComponent {
protected hasPermissionToCreateApiKey: boolean;
protected hasPermissionToUpdateUserSettings: boolean;
protected price: number;
protected readonly trySubscriptionMailHref = `mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=${[
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
].join(E_MAIL_LINE_BREAK)}`;
protected readonly trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
protected user: User;
private couponId: string | undefined;

5
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -50,7 +50,10 @@
}
}
<div class="align-items-center d-flex justify-content-center mt-4">
@if (!user?.subscription?.subscribedAt) {
@if (
!user?.subscription?.subscribedAt &&
hasPermissionToUpdateUserSettings
) {
<a
class="mx-1"
mat-stroked-button

2
apps/client/src/app/services/web-authn.service.ts

@ -66,7 +66,7 @@ export class WebAuthnService {
const deviceId = this.getDeviceId();
return this.http
.delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`)
.delete<AuthDeviceDto>(`/api/v1/auth-devices/${deviceId}`)
.pipe(
catchError((error) => {
console.warn(`Could not deregister device ${deviceId}`, error);

14
libs/common/src/lib/config.ts

@ -422,4 +422,18 @@ export const THROTTLE_DEFAULT_TTL = ms('1 minute');
export const THROTTLE_SIGNUP_LIMIT = 5;
export const THROTTLE_SIGNUP_TTL = ms('1 hour');
export const TRY_SUBSCRIPTION_MAIL_HREF = `mailto:hi@ghostfol.io?subject=${encodeURIComponent(
'Ghostfolio Premium Trial'
)}&body=${[
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
]
.map((line) => {
return encodeURIComponent(line);
})
.join(E_MAIL_LINE_BREAK)}`;
export const UNKNOWN_KEY = 'UNKNOWN';

2
libs/common/src/lib/interfaces/index.ts

@ -67,6 +67,7 @@ import type {
} from './responses/data-provider-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface';
import type { ResponseError } from './responses/errors.interface';
import type { ExchangeRateResponse } from './responses/exchange-rate-response.interface';
import type { ExportResponse } from './responses/export-response.interface';
import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface';
@ -150,6 +151,7 @@ export {
DataProviderResponse,
DividendsResponse,
EnhancedAssetProfile,
ExchangeRateResponse,
ExportResponse,
Filter,
FilterGroup,

3
libs/common/src/lib/interfaces/responses/exchange-rate-response.interface.ts

@ -0,0 +1,3 @@
export interface ExchangeRateResponse {
marketPrice: number;
}

7
libs/ui/src/lib/dialog-footer/dialog-footer.component.html

@ -1,7 +1,10 @@
@if (deviceType === 'mobile') {
<div class="d-flex justify-content-center" mat-dialog-actions>
<button mat-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" />
<button mat-icon-button (click)="onClickCloseButton()">
<ion-icon
class="d-block mat-icon text-body-secondary"
name="close-outline"
/>
</button>
</div>
}

4
libs/ui/src/lib/dialog-footer/dialog-footer.component.scss

@ -1,3 +1,7 @@
:host {
display: block;
button[mat-icon-button] {
--mat-icon-button-touch-target-display: block;
}
}

4
libs/ui/src/lib/dialog-footer/dialog-footer.component.ts

@ -9,7 +9,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons';
import { closeOutline } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -25,7 +25,7 @@ export class GfDialogFooterComponent {
@Output() closeButtonClicked = new EventEmitter<void>();
public constructor() {
addIcons({ close });
addIcons({ closeOutline });
}
public onClickCloseButton() {

13
libs/ui/src/lib/dialog-header/dialog-header.component.html

@ -1,12 +1,19 @@
<div class="d-flex" mat-dialog-title>
<div
class="d-flex"
mat-dialog-title
[class.with-close-button]="deviceType() !== 'mobile'"
>
<span
class="flex-grow-1 text-truncate"
[class.text-center]="position() === 'center'"
>{{ title() }}</span
>
@if (deviceType() !== 'mobile') {
<button class="no-min-width px-0" mat-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" />
<button mat-icon-button (click)="onClickCloseButton()">
<ion-icon
class="d-block mat-icon text-body-secondary"
name="close-outline"
/>
</button>
}
</div>

11
libs/ui/src/lib/dialog-header/dialog-header.component.scss

@ -1,3 +1,14 @@
:host {
display: block;
button[mat-icon-button] {
margin-right: -0.8125rem;
margin-top: 0.125rem;
}
.with-close-button {
.text-center {
padding-left: 0.9375rem;
}
}
}

4
libs/ui/src/lib/dialog-header/dialog-header.component.ts

@ -8,7 +8,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons';
import { closeOutline } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -26,7 +26,7 @@ export class GfDialogHeaderComponent {
public readonly closeButtonClicked = output<void>();
public constructor() {
addIcons({ close });
addIcons({ closeOutline });
}
protected onClickCloseButton() {

13
libs/ui/src/lib/services/data.service.ts

@ -40,7 +40,6 @@ import {
CreateActivityResponse,
CreateStripeCheckoutSessionResponse,
DataProviderHealthResponse,
DataProviderHistoricalResponse,
ExportResponse,
Filter,
ImportResponse,
@ -309,18 +308,6 @@ export class DataService {
);
}
public fetchExchangeRateForDate({
date,
symbol
}: {
date: Date;
symbol: string;
}) {
return this.http.get<DataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${encodeURIComponent(symbol)}/${format(date, DATE_FORMAT, { in: utc })}`
);
}
public deleteAccess(aId: string) {
return this.http.delete<void>(`/api/v1/access/${aId}`);
}

30
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.89.0",
"version": "3.90.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.89.0",
"version": "3.90.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -104,7 +104,7 @@
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.1",
"undici": "8.11.2",
"undici": "8.12.2",
"uuid": "14.0.2",
"yahoo-finance2": "4.0.2",
"zod": "4.6.5",
@ -149,7 +149,7 @@
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12",
"@types/node": "22.20.1",
"@types/node": "22.20.5",
"@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3",
@ -163,7 +163,7 @@
"jest-environment-node": "30.3.0",
"jest-preset-angular": "17.0.0",
"nx": "23.2.1",
"prettier": "3.9.9",
"prettier": "3.9.10",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.10.0",
"react": "18.2.0",
@ -180,7 +180,7 @@
"webpack-cli": "7.1.0"
},
"engines": {
"node": ">=22.22.3"
"node": ">=22.23.4"
}
},
"node_modules/@adobe/css-tools": {
@ -14306,9 +14306,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
"version": "22.20.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
"integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -29600,9 +29600,9 @@
}
},
"node_modules/prettier": {
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
"integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
"dev": true,
"license": "MIT",
"bin": {
@ -33913,9 +33913,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz",
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==",
"version": "8.12.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
"integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"

10
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.89.0",
"version": "3.90.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -148,7 +148,7 @@
"svgmap": "2.21.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.1",
"undici": "8.11.2",
"undici": "8.12.2",
"uuid": "14.0.2",
"yahoo-finance2": "4.0.2",
"zod": "4.6.5",
@ -193,7 +193,7 @@
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12",
"@types/node": "22.20.1",
"@types/node": "22.20.5",
"@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3",
@ -207,7 +207,7 @@
"jest-environment-node": "30.3.0",
"jest-preset-angular": "17.0.0",
"nx": "23.2.1",
"prettier": "3.9.9",
"prettier": "3.9.10",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.10.0",
"react": "18.2.0",
@ -224,6 +224,6 @@
"webpack-cli": "7.1.0"
},
"engines": {
"node": ">=22.22.3"
"node": ">=22.23.4"
}
}

Loading…
Cancel
Save