Browse Source

Merge remote-tracking branch 'upstream/main' into feature/public-access-filter

pull/5848/head
Germán Martín 1 week ago
parent
commit
7bfa7900ea
  1. 43
      CHANGELOG.md
  2. 6
      README.md
  3. 2
      apps/api/src/app/app.module.ts
  4. 22
      apps/api/src/app/endpoints/platforms/platforms.controller.ts
  5. 11
      apps/api/src/app/endpoints/platforms/platforms.module.ts
  6. 1
      apps/api/src/app/info/info.service.ts
  7. 1
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 5
      apps/api/src/app/subscription/subscription.service.ts
  9. 22
      apps/api/src/helper/object.helper.spec.ts
  10. 11
      apps/api/src/helper/object.helper.ts
  11. 1
      apps/api/src/services/configuration/configuration.service.ts
  12. 2
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  13. 11
      apps/api/src/services/data-provider/manual/manual.service.ts
  14. 1
      apps/api/src/services/interfaces/environment.interface.ts
  15. 1
      apps/api/tsconfig.app.json
  16. 1
      apps/api/tsconfig.spec.json
  17. 1
      apps/client/project.json
  18. 2
      apps/client/src/app/components/access-table/access-table.component.ts
  19. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  20. 26
      apps/client/src/app/components/admin-users/admin-users.component.ts
  21. 6
      apps/client/src/app/components/admin-users/admin-users.html
  22. 3
      apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts
  23. 3
      apps/client/src/app/components/footer/footer.component.ts
  24. 2
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts
  25. 11
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  26. 2
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  27. 20
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  28. 2
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  29. 5
      apps/client/src/app/pages/admin/admin-page.routes.ts
  30. 3
      apps/client/src/app/pages/blog/blog-page.component.ts
  31. 17
      apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html
  32. 3
      apps/client/src/app/pages/markets/markets-page.component.ts
  33. 3
      apps/client/src/app/pages/open/open-page.component.ts
  34. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  35. 24
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  36. 2
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts
  37. 3
      apps/client/src/app/pages/zen/zen-page.component.ts
  38. 6
      apps/client/src/app/services/data.service.ts
  39. 6
      apps/client/src/locales/messages.de.xlf
  40. 15
      apps/client/src/main.ts
  41. 4
      apps/client/src/test-setup.ts
  42. 1
      apps/client/tsconfig.app.json
  43. 2
      apps/client/tsconfig.json
  44. 3
      apps/client/tsconfig.spec.json
  45. 2
      libs/common/src/lib/interfaces/index.ts
  46. 6
      libs/common/src/lib/interfaces/info-item.interface.ts
  47. 1
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  48. 3
      libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts
  49. 5
      libs/common/src/lib/interfaces/responses/platforms-response.interface.ts
  50. 2
      libs/common/src/lib/permissions.ts
  51. 2
      libs/common/src/lib/routes/routes.ts
  52. 6
      libs/common/tsconfig.json
  53. 3
      libs/common/tsconfig.spec.json
  54. 0
      libs/ui/.storybook/main.mjs
  55. 7
      libs/ui/project.json
  56. 4
      libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
  57. 8
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  58. 2
      libs/ui/src/lib/logo-carousel/logo-carousel.component.ts
  59. 4
      libs/ui/src/test-setup.ts
  60. 2
      libs/ui/tsconfig.json
  61. 2
      libs/ui/tsconfig.lib.json
  62. 3
      libs/ui/tsconfig.spec.json
  63. 38959
      package-lock.json
  64. 105
      package.json
  65. 3
      tsconfig.base.json

43
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 2.227.0 - 2026-01-02
### Added
@ -13,12 +13,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Initialized the input properties in the _FIRE_ calculator
- Removed the deprecated public _Stripe_ key
- Upgraded `stripe` from version `18.5.0` to `20.1.0`
### Fixed
- Fixed the import of `jsonpath` to support REST APIs (`JSON`) via the scraper configuration
## 2.226.0 - 2026-01-01
### Added
- Extended the content of the _Self-Hosting_ section by information about additional data providers on the Frequently Asked Questions (FAQ) page
### Changed
- Upgraded `class-validator` from version `0.14.2` to `0.14.3`
- Upgraded `yahoo-finance2` from version `3.10.2` to `3.11.2`
## 2.225.0 - 2025-12-31
### Added
- Added a new endpoint to get all platforms (`GET api/v1/platforms`)
- Added the session url to the endpoint response of the _Stripe_ checkout
### Changed
- Improved the routing of the user detail dialog in the users section of the admin control panel
- Lifted the asset profile identifier editing restriction for `MANUAL` data sources in the asset profile details dialog of the admin control panel
- Deprecated the public _Stripe_ key
- Improved the language localization for German (`de`)
- Eliminated `ngx-stripe`
- Upgraded `angular` from version `20.2.4` to `21.0.6`
- Upgraded `marked` from version `15.0.4` to `17.0.1`
- Upgraded `ngx-device-detector` from version `10.1.0` to `11.0.0`
- Upgraded `ng-extract-i18n-merge` from `3.1.0` to `3.2.1`
- Upgraded `ngx-markdown` from version `20.0.0` to `21.0.1`
- Upgraded `Nx` from version `21.5.1` to `22.3.3`
- Upgraded `shx` from version `0.3.4` to `0.4.0`
- Upgraded `storybook` from version `9.1.5` to `10.1.10`
- Upgraded `zone.js` from version `0.15.1` to `0.16.0`
### Fixed
- Added the missing currency suffix to the cash balance field in the create or update account dialog
- Fixed the time in market display of the portfolio summary tab on the home page for the impersonation mode
- Fixed the delete button in the asset profile details dialog of the admin control panel by providing the missing `watchedByCount` parameter
## 2.224.2 - 2025-12-20

6
README.md

@ -241,7 +241,7 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
| `accountId` | `string` (optional) | Id of the account |
| `comment` | `string` (optional) | Comment of the activity |
| `currency` | `string` | `CHF` \| `EUR` \| `USD` etc. |
| `dataSource` | `string` | `COINGECKO` \| `MANUAL` \| `YAHOO` |
| `dataSource` | `string` | `COINGECKO` \| `GHOSTFOLIO` [^1] \| `MANUAL` \| `YAHOO` |
| `date` | `string` | Date in the format `ISO-8601` |
| `fee` | `number` | Fee of the activity |
| `quantity` | `number` | Quantity of the activity |
@ -331,6 +331,8 @@ If you like to support this project, become a [**Sponsor**](https://github.com/s
## License
© 2021 - 2025 [Ghostfolio](https://ghostfol.io)
© 2021 - 2026 [Ghostfolio](https://ghostfol.io)
Licensed under the [AGPLv3 License](https://www.gnu.org/licenses/agpl-3.0.html).
[^1]: Available with [**Ghostfolio Premium**](https://ghostfol.io/en/pricing).

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

@ -37,6 +37,7 @@ import { AssetsModule } from './endpoints/assets/assets.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { PlatformsModule } from './endpoints/platforms/platforms.module';
import { PublicModule } from './endpoints/public/public.module';
import { SitemapModule } from './endpoints/sitemap/sitemap.module';
import { TagsModule } from './endpoints/tags/tags.module';
@ -95,6 +96,7 @@ import { UserModule } from './user/user.module';
MarketDataModule,
OrderModule,
PlatformModule,
PlatformsModule,
PortfolioModule,
PortfolioSnapshotQueueModule,
PrismaModule,

22
apps/api/src/app/endpoints/platforms/platforms.controller.ts

@ -0,0 +1,22 @@
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { PlatformsResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('platforms')
export class PlatformsController {
public constructor(private readonly platformService: PlatformService) {}
@Get()
@HasPermission(permissions.readPlatforms)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPlatforms(): Promise<PlatformsResponse> {
const platforms = await this.platformService.getPlatforms();
return { platforms };
}
}

11
apps/api/src/app/endpoints/platforms/platforms.module.ts

@ -0,0 +1,11 @@
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
import { Module } from '@nestjs/common';
import { PlatformsController } from './platforms.controller';
@Module({
controllers: [PlatformsController],
imports: [PlatformModule]
})
export class PlatformsModule {}

1
apps/api/src/app/info/info.service.ts

@ -93,7 +93,6 @@ export class InfoService {
(await this.propertyService.getByKey<string[]>(
PROPERTY_COUNTRIES_OF_SUBSCRIBERS
)) ?? [];
info.stripePublicKey = this.configurationService.get('STRIPE_PUBLIC_KEY');
}
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {

1
apps/api/src/app/portfolio/portfolio.service.ts

@ -1954,6 +1954,7 @@ export class PortfolioService {
}).length,
committedFunds: committedFunds.toNumber(),
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
dateOfFirstActivity: firstOrderDate,
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
emergencyFund: {
assets: emergencyFundHoldingsValueInBaseCurrency,

5
apps/api/src/app/subscription/subscription.service.ts

@ -35,7 +35,7 @@ export class SubscriptionService {
this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'),
{
apiVersion: '2025-08-27.basil'
apiVersion: '2025-12-15.clover'
}
);
}
@ -100,7 +100,8 @@ export class SubscriptionService {
);
return {
sessionId: session.id
sessionId: session.id,
sessionUrl: session.url
};
}

22
apps/api/src/helper/object.helper.spec.ts

@ -1,4 +1,24 @@
import { redactAttributes } from './object.helper';
import { query, redactAttributes } from './object.helper';
describe('query', () => {
it('should get market price from stock API response', () => {
const object = {
currency: 'USD',
market: {
previousClose: 273.04,
price: 271.86
},
symbol: 'AAPL'
};
const result = query({
object,
pathExpression: '$.market.price'
})[0];
expect(result).toBe(271.86);
});
});
describe('redactAttributes', () => {
it('should redact provided attributes', () => {

11
apps/api/src/helper/object.helper.ts

@ -1,4 +1,5 @@
import { Big } from 'big.js';
import jsonpath from 'jsonpath';
import { cloneDeep, isArray, isObject } from 'lodash';
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
@ -31,6 +32,16 @@ export function nullifyValuesInObjects<T>(aObjects: T[], keys: string[]): T[] {
});
}
export function query({
object,
pathExpression
}: {
object: object;
pathExpression: string;
}) {
return jsonpath.query(object, pathExpression);
}
export function redactAttributes({
isFirstRun = true,
object,

1
apps/api/src/services/configuration/configuration.service.ts

@ -102,7 +102,6 @@ export class ConfigurationService {
ROOT_URL: url({
default: environment.rootUrl
}),
STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),
TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }),

2
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -18,7 +18,7 @@ import {
import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage';
import Alphavantage from 'alphavantage';
import { format, isAfter, isBefore, parse } from 'date-fns';
import { AlphaVantageHistoricalResponse } from './interfaces/interfaces';

11
apps/api/src/services/data-provider/manual/manual.service.ts

@ -1,3 +1,4 @@
import { query } from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@ -26,7 +27,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import * as cheerio from 'cheerio';
import { addDays, format, isBefore } from 'date-fns';
import * as jsonpath from 'jsonpath';
@Injectable()
export class ManualService implements DataProviderInterface {
@ -286,9 +286,14 @@ export class ManualService implements DataProviderInterface {
let value: string;
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.json();
const object = await response.json();
value = String(jsonpath.query(data, scraperConfiguration.selector)[0]);
value = String(
query({
object,
pathExpression: scraperConfiguration.selector
})[0]
);
} else {
const $ = cheerio.load(await response.text());

1
apps/api/src/services/interfaces/environment.interface.ts

@ -52,7 +52,6 @@ export interface Environment extends CleanedEnvAccessors {
REDIS_PORT: number;
REQUEST_TIMEOUT: number;
ROOT_URL: string;
STRIPE_PUBLIC_KEY: string;
STRIPE_SECRET_KEY: string;
TWITTER_ACCESS_TOKEN: string;
TWITTER_ACCESS_TOKEN_SECRET: string;

1
apps/api/tsconfig.app.json

@ -4,6 +4,7 @@
"outDir": "../../dist/out-tsc",
"types": ["node"],
"emitDecoratorMetadata": true,
"moduleResolution": "node10",
"target": "es2021",
"module": "commonjs"
},

1
apps/api/tsconfig.spec.json

@ -3,6 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]

1
apps/client/project.json

@ -75,7 +75,6 @@
"ngswConfigPath": "apps/client/ngsw-config.json",
"optimization": false,
"polyfills": "apps/client/src/polyfills.ts",
"scripts": ["node_modules/marked/marked.min.js"],
"serviceWorker": true,
"sourceMap": true,
"styles": [

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

@ -4,7 +4,6 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -36,7 +35,6 @@ import ms from 'ms';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ClipboardModule,
CommonModule,
IonIcon,
MatButtonModule,
MatMenuModule,

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -29,7 +29,6 @@ import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplet
import { GfValueComponent } from '@ghostfolio/ui/value';
import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectionStrategy,
@ -95,7 +94,6 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
FormsModule,
GfCurrencySelectorComponent,
GfEntityLogoComponent,

26
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -18,6 +18,7 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -39,7 +40,7 @@ import {
PageEvent
} from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import {
differenceInSeconds,
@ -69,7 +70,8 @@ import { takeUntil } from 'rxjs/operators';
MatMenuModule,
MatPaginatorModule,
MatTableModule,
NgxSkeletonLoaderModule
NgxSkeletonLoaderModule,
RouterModule
],
selector: 'gf-admin-users',
styleUrls: ['./admin-users.scss'],
@ -88,6 +90,8 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public info: InfoItem;
public isLoading = false;
public pageSize = DEFAULT_PAGE_SIZE;
public routerLinkAdminControlUsers =
internalRoutes.adminControl.subRoutes.users.routerLink;
public totalItems = 0;
public user: User;
@ -136,11 +140,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
];
}
this.route.queryParams
this.route.paramMap
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['userDetailDialog'] && params['userId']) {
this.openUserDetailDialog(params['userId']);
const userId = params.get('userId');
if (userId) {
this.openUserDetailDialog(userId);
}
});
@ -248,9 +254,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
}
public onOpenUserDetailDialog(userId: string) {
this.router.navigate([], {
queryParams: { userId, userDetailDialog: true }
});
this.router.navigate(
internalRoutes.adminControl.subRoutes.users.routerLink.concat(userId)
);
}
public ngOnDestroy() {
@ -301,7 +307,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
this.router.navigate(
internalRoutes.adminControl.subRoutes.users.routerLink
);
});
}
}

6
apps/client/src/app/components/admin-users/admin-users.html

@ -215,9 +215,9 @@
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #userMenu="matMenu" xPosition="before">
<button
<a
mat-menu-item
(click)="onOpenUserDetailDialog(element.id)"
[routerLink]="routerLinkAdminControlUsers.concat(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="person-outline" />
@ -225,7 +225,7 @@
><ng-container i18n>View Details</ng-container>...</span
>
</span>
</button>
</a>
@if (hasPermissionToImpersonateAllUsers) {
<button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex">

3
apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts

@ -1,7 +1,6 @@
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -12,7 +11,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, NgxSkeletonLoaderModule],
imports: [NgxSkeletonLoaderModule],
selector: 'gf-fear-and-greed-index',
styleUrls: ['./fear-and-greed-index.component.scss'],
templateUrl: './fear-and-greed-index.component.html'

3
apps/client/src/app/components/footer/footer.component.ts

@ -3,7 +3,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -18,7 +17,7 @@ import { openOutline } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, GfLogoComponent, IonIcon, RouterModule],
imports: [GfLogoComponent, IonIcon, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-footer',
styleUrls: ['./footer.component.scss'],

2
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts

@ -4,7 +4,6 @@ import {
} from '@ghostfolio/client/services/settings-storage.service';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@ -28,7 +27,6 @@ import { LoginWithAccessTokenDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfDialogHeaderComponent,
IonIcon,
MatButtonModule,

11
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -84,10 +84,13 @@ export class GfPortfolioSummaryComponent implements OnChanges {
this.precision = 0;
}
if (this.user.dateOfFirstActivity) {
this.timeInMarket = formatDistanceToNow(this.user.dateOfFirstActivity, {
locale: getDateFnsLocale(this.language)
});
if (this.summary.dateOfFirstActivity) {
this.timeInMarket = formatDistanceToNow(
this.summary.dateOfFirstActivity,
{
locale: getDateFnsLocale(this.language)
}
);
} else {
this.timeInMarket = '-';
}

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

@ -2,7 +2,6 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -31,7 +30,6 @@ import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column flex-grow-1 h-100' },
imports: [
CommonModule,
GfMembershipCardComponent,
GfPremiumIndicatorComponent,
IonIcon,

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

@ -21,9 +21,8 @@ import { MatCardModule } from '@angular/material/card';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
import ms, { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
import { catchError, takeUntil } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -62,7 +61,6 @@ export class GfUserAccountMembershipComponent implements OnDestroy {
private dataService: DataService,
private notificationService: NotificationService,
private snackBar: MatSnackBar,
private stripeService: StripeService,
private userService: UserService
) {
const { baseCurrency, globalPermissions } = this.dataService.fetchInfo();
@ -113,23 +111,17 @@ export class GfUserAccountMembershipComponent implements OnDestroy {
priceId: this.priceId
})
.pipe(
catchError((error) => {
catchError((error: Error) => {
this.notificationService.alert({
title: error.message
});
throw error;
return EMPTY;
}),
switchMap(({ sessionId }) => {
return this.stripeService.redirectToCheckout({ sessionId });
})
takeUntil(this.unsubscribeSubject)
)
.subscribe((result) => {
if (result.error) {
this.notificationService.alert({
title: result.error.message
});
}
.subscribe(({ sessionUrl }) => {
window.location.href = sessionUrl;
});
}

2
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -4,7 +4,6 @@ import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -26,7 +25,6 @@ import { UserDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfValueComponent,

5
apps/client/src/app/pages/admin/admin-page.routes.ts

@ -38,6 +38,11 @@ export const routes: Routes = [
path: internalRoutes.adminControl.subRoutes.users.path,
component: GfAdminUsersComponent,
title: internalRoutes.adminControl.subRoutes.users.title
},
{
path: `${internalRoutes.adminControl.subRoutes.users.path}/:userId`,
component: GfAdminUsersComponent,
title: internalRoutes.adminControl.subRoutes.users.title
}
],
component: AdminPageComponent,

3
apps/client/src/app/pages/blog/blog-page.component.ts

@ -1,7 +1,6 @@
import { DataService } from '@ghostfolio/client/services/data.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { CommonModule } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA, OnDestroy } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
@ -12,7 +11,7 @@ import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule, IonIcon, MatCardModule, RouterModule],
imports: [IonIcon, MatCardModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-blog-page',
styleUrls: ['./blog-page.scss'],

17
apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html

@ -135,9 +135,17 @@
<mat-card-title>Which data providers are supported?</mat-card-title>
</mat-card-header>
<mat-card-content
><code>COINGECKO</code>, <code>MANUAL</code> and
<code>YAHOO</code> are officially supported while all other data
providers are considered experimental.</mat-card-content
><code>COINGECKO</code>, <code>GHOSTFOLIO</code> (available with
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
>Ghostfolio Premium<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false" /></a
>), <code>MANUAL</code>, and <code>YAHOO</code> are officially
supported while all other data providers are considered
experimental.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
@ -147,7 +155,8 @@
>
</mat-card-header>
<mat-card-content
>Yes, access to a professional data provider is included with a
>Yes, access to a professional data provider (<code>GHOSTFOLIO</code>)
is included with a
<a
class="align-items-center d-inline-flex"
target="_blank"

3
apps/client/src/app/pages/markets/markets-page.component.ts

@ -1,12 +1,11 @@
import { GfHomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { CommonModule } from '@angular/common';
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule, GfHomeMarketComponent],
imports: [GfHomeMarketComponent],
selector: 'gf-markets-page',
styleUrls: ['./markets-page.scss'],
templateUrl: './markets-page.html'

3
apps/client/src/app/pages/open/open-page.component.ts

@ -3,7 +3,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Statistics, User } from '@ghostfolio/common/interfaces';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -16,7 +15,7 @@ import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule, GfValueComponent, MatCardModule],
imports: [GfValueComponent, MatCardModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-open-page',
styleUrls: ['./open-page.scss'],

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -96,7 +96,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
public isLoading = false;
public isToday = isToday;
public mode: 'create' | 'update';
public platforms: { id: string; name: string }[];
public tagsAvailable: Tag[] = [];
public total = 0;
public typesTranslationMap = new Map<Type, string>();
@ -127,11 +126,10 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy {
this.dateAdapter.setLocale(this.locale);
const { currencies, platforms } = this.dataService.fetchInfo();
const { currencies } = this.dataService.fetchInfo();
this.currencies = currencies;
this.defaultDateFormat = getDateFormatString(this.locale);
this.platforms = platforms;
this.dataService
.fetchPortfolioHoldings()

24
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -27,9 +27,8 @@ import {
informationCircleOutline
} from 'ionicons/icons';
import { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
@ -98,7 +97,6 @@ export class GfPricingPageComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private notificationService: NotificationService,
private stripeService: StripeService,
private userService: UserService
) {
addIcons({
@ -155,23 +153,17 @@ export class GfPricingPageComponent implements OnDestroy, OnInit {
priceId: this.priceId
})
.pipe(
switchMap(({ sessionId }) => {
return this.stripeService.redirectToCheckout({ sessionId });
}),
catchError((error) => {
catchError((error: Error) => {
this.notificationService.alert({
title: error.message
});
throw error;
})
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe((result) => {
if (result.error) {
this.notificationService.alert({
title: result.error.message
});
}
.subscribe(({ sessionUrl }) => {
window.location.href = sessionUrl;
});
}

2
apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts

@ -3,7 +3,6 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -36,7 +35,6 @@ import { UserAccountRegistrationDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ClipboardModule,
CommonModule,
FormsModule,
IonIcon,
MatButtonModule,

3
apps/client/src/app/pages/zen/zen-page.component.ts

@ -2,7 +2,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
@ -15,7 +14,7 @@ import { takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page has-tabs' },
imports: [CommonModule, IonIcon, MatTabsModule, RouterModule],
imports: [IonIcon, MatTabsModule, RouterModule],
selector: 'gf-zen-page',
styleUrls: ['./zen-page.scss'],
templateUrl: './zen-page.html'

6
apps/client/src/app/services/data.service.ts

@ -564,6 +564,12 @@ export class DataService {
}
}
if (response.summary?.dateOfFirstActivity) {
response.summary.dateOfFirstActivity = parseISO(
response.summary.dateOfFirstActivity
);
}
return response;
})
);

6
apps/client/src/locales/messages.de.xlf

@ -3475,7 +3475,7 @@
</trans-unit>
<trans-unit id="5499742151525073097" datatype="html">
<source>Upgrade Plan</source>
<target state="translated">Abonnement abschliessen</target>
<target state="translated">Mitgliedschaft abschliessen</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/header/header.component.html</context>
<context context-type="linenumber">193</context>
@ -3747,7 +3747,7 @@
</trans-unit>
<trans-unit id="2030314101752312029" datatype="html">
<source>Renew Plan</source>
<target state="translated">Abonnement erneuern</target>
<target state="translated">Mitgliedschaft erneuern</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/header/header.component.html</context>
<context context-type="linenumber">191</context>
@ -6934,7 +6934,7 @@
</trans-unit>
<trans-unit id="7522916136412124285" datatype="html">
<source>to use our referral link and get a Ghostfolio Premium membership for one year</source>
<target state="translated">um unseren Empfehlungslink zu verwenden und ein Ghostfolio Premium-Abonnement für ein Jahr zu erhalten</target>
<target state="translated">um unseren Empfehlungslink zu verwenden und eine Ghostfolio Premium-Mitgliedschaft für ein Jahr zu erhalten</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">343</context>

15
apps/client/src/main.ts

@ -7,7 +7,11 @@ import {
provideHttpClient,
withInterceptorsFromDi
} from '@angular/common/http';
import { enableProdMode, importProvidersFrom } from '@angular/core';
import {
enableProdMode,
importProvidersFrom,
provideZoneChangeDetection
} from '@angular/core';
import {
DateAdapter,
MAT_DATE_FORMATS,
@ -23,7 +27,6 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { provideIonicAngular } from '@ionic/angular/standalone';
import { provideMarkdown } from 'ngx-markdown';
import { provideNgxSkeletonLoader } from 'ngx-skeleton-loader';
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
import { CustomDateAdapter } from './app/adapter/custom-date-adapter';
import { DateFormats } from './app/adapter/date-formats';
@ -50,8 +53,6 @@ import { environment } from './environments/environment';
(window as any).info = info;
environment.stripePublicKey = info.stripePublicKey;
if (environment.production) {
enableProdMode();
}
@ -65,7 +66,6 @@ import { environment } from './environments/environment';
MatNativeDateModule,
MatSnackBarModule,
MatTooltipModule,
NgxStripeModule.forRoot(environment.stripePublicKey),
RouterModule.forRoot(routes, {
anchorScrolling: 'enabled',
preloadingStrategy: ModulePreloadService,
@ -83,6 +83,7 @@ import { environment } from './environments/environment';
provideIonicAngular(),
provideMarkdown(),
provideNgxSkeletonLoader(),
provideZoneChangeDetection(),
{
deps: [LanguageService, MAT_DATE_LOCALE, Platform],
provide: DateAdapter,
@ -92,10 +93,6 @@ import { environment } from './environments/environment';
provide: MAT_DATE_FORMATS,
useValue: DateFormats
},
{
provide: STRIPE_PUBLISHABLE_KEY,
useFactory: () => environment.stripePublicKey
},
{
provide: TitleStrategy,
useClass: PageTitleStrategy

4
apps/client/src/test-setup.ts

@ -1 +1,3 @@
import 'jest-preset-angular/setup-jest';
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();

1
apps/client/tsconfig.app.json

@ -1,7 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"outDir": "../../dist/out-tsc",
"types": ["node"],
"typeRoots": ["../../node_modules/@types"],

2
apps/client/tsconfig.json

@ -20,6 +20,8 @@
"strictTemplates": false
},
"compilerOptions": {
"lib": ["dom", "es2022"],
"module": "preserve",
"target": "es2020"
}
}

3
apps/client/tsconfig.spec.json

@ -2,7 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"module": "preserve",
"isolatedModules": true,
"types": ["jest", "node"],
"target": "es2016"
},

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

@ -68,6 +68,7 @@ import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PlatformsResponse } from './responses/platforms-response.interface';
import type { PortfolioDividendsResponse } from './responses/portfolio-dividends-response.interface';
import type { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
@ -160,6 +161,7 @@ export {
MarketDataDetailsResponse,
MarketDataOfMarketsResponse,
OAuthResponse,
PlatformsResponse,
PortfolioChart,
PortfolioDetails,
PortfolioDividendsResponse,

6
libs/common/src/lib/interfaces/info-item.interface.ts

@ -13,8 +13,14 @@ export interface InfoItem {
globalPermissions: string[];
isDataGatheringEnabled?: string;
isReadOnlyMode?: boolean;
/** @deprecated */
platforms: Platform[];
statistics: Statistics;
/** @deprecated */
stripePublicKey?: string;
subscriptionOffer?: SubscriptionOffer;
}

1
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -7,6 +7,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercentWithCurrencyEffect: number;
cash: number;
committedFunds: number;
dateOfFirstActivity: Date;
dividendInBaseCurrency: number;
emergencyFund: {
assets: number;

3
libs/common/src/lib/interfaces/responses/create-stripe-checkout-session-response.interface.ts

@ -1,3 +1,6 @@
export interface CreateStripeCheckoutSessionResponse {
/** @deprecated */
sessionId: string;
sessionUrl: string;
}

5
libs/common/src/lib/interfaces/responses/platforms-response.interface.ts

@ -0,0 +1,5 @@
import { Platform } from '@prisma/client';
export interface PlatformsResponse {
platforms: Platform[];
}

2
libs/common/src/lib/permissions.ts

@ -93,6 +93,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.readAiPrompt,
permissions.readMarketData,
permissions.readMarketDataOfOwnAssetProfile,
permissions.readPlatforms,
permissions.readPlatformsWithAccountCount,
permissions.readTags,
permissions.readWatchlist,
@ -136,6 +137,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.deleteWatchlistItem,
permissions.readAiPrompt,
permissions.readMarketDataOfOwnAssetProfile,
permissions.readPlatforms,
permissions.readWatchlist,
permissions.updateAccount,
permissions.updateAccess,

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

@ -5,7 +5,7 @@ import { InternalRoute } from './interfaces/internal-route.interface';
import { PublicRoute } from './interfaces/public-route.interface';
if (typeof window !== 'undefined') {
import('@angular/localize/init');
import('@angular/localize');
} else {
(global as any).$localize = (
messageParts: TemplateStringsArray,

6
libs/common/tsconfig.json

@ -9,5 +9,9 @@
{
"path": "./tsconfig.spec.json"
}
]
],
"compilerOptions": {
"module": "preserve",
"lib": ["dom", "es2022"]
}
}

3
libs/common/tsconfig.spec.json

@ -2,7 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"module": "preserve",
"isolatedModules": true,
"types": ["jest", "node"]
},
"include": [

0
libs/ui/.storybook/main.js → libs/ui/.storybook/main.mjs

7
libs/ui/project.json

@ -31,12 +31,7 @@
"browserTarget": "ui:build-storybook",
"compodoc": false,
"configDir": "libs/ui/.storybook",
"port": 4400,
"styles": [
"apps/client/src/assets/fonts/inter.css",
"apps/client/src/styles/theme.scss",
"apps/client/src/styles.scss"
]
"port": 4400
},
"configurations": {
"ci": {

4
libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts

@ -44,8 +44,10 @@ type Story = StoryObj<GfFireCalculatorComponent>;
export const Simple: Story = {
args: {
annualInterestRate: 5,
currency: 'USD',
fireWealth: 50000,
locale: locale
locale: locale,
savingsRate: 1000
}
};

8
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -77,16 +77,16 @@ import { FireCalculatorService } from './fire-calculator.service';
templateUrl: './fire-calculator.component.html'
})
export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() annualInterestRate: number;
@Input() annualInterestRate = 0;
@Input() colorScheme: ColorScheme;
@Input() currency: string;
@Input() deviceType: string;
@Input() fireWealth: number;
@Input() fireWealth = 0;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() locale = getLocale();
@Input() projectedTotalAmount: number;
@Input() projectedTotalAmount = 0;
@Input() retirementDate: Date;
@Input() savingsRate: number;
@Input() savingsRate = 0;
@Output() annualInterestRateChanged = new EventEmitter<number>();
@Output() calculationCompleted =

2
libs/ui/src/lib/logo-carousel/logo-carousel.component.ts

@ -1,11 +1,9 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LogoItem } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
selector: 'gf-logo-carousel',
styleUrls: ['./logo-carousel.component.scss'],
templateUrl: './logo-carousel.component.html'

4
libs/ui/src/test-setup.ts

@ -1 +1,3 @@
import 'jest-preset-angular/setup-jest';
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();

2
libs/ui/tsconfig.json

@ -14,6 +14,8 @@
}
],
"compilerOptions": {
"lib": ["dom", "es2022"],
"module": "preserve",
"target": "es2020",
// TODO: Remove once solved in tsconfig.base.json
"strict": false,

2
libs/ui/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"]
"lib": ["dom", "es2022"]
},
"exclude": [
"src/test-setup.ts",

3
libs/ui/tsconfig.spec.json

@ -2,7 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"module": "preserve",
"isolatedModules": true,
"types": ["jest", "node"],
"target": "es2016"
},

38959
package-lock.json

File diff suppressed because it is too large

105
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.224.2",
"version": "2.227.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -54,17 +54,17 @@
"workspace-generator": "nx workspace-generator"
},
"dependencies": {
"@angular/animations": "20.2.4",
"@angular/cdk": "20.2.2",
"@angular/common": "20.2.4",
"@angular/compiler": "20.2.4",
"@angular/core": "20.2.4",
"@angular/forms": "20.2.4",
"@angular/material": "20.2.2",
"@angular/platform-browser": "20.2.4",
"@angular/platform-browser-dynamic": "20.2.4",
"@angular/router": "20.2.4",
"@angular/service-worker": "20.2.4",
"@angular/animations": "21.0.6",
"@angular/cdk": "21.0.5",
"@angular/common": "21.0.6",
"@angular/compiler": "21.0.6",
"@angular/core": "21.0.6",
"@angular/forms": "21.0.6",
"@angular/material": "21.0.5",
"@angular/platform-browser": "21.0.6",
"@angular/platform-browser-dynamic": "21.0.6",
"@angular/router": "21.0.6",
"@angular/service-worker": "21.0.6",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0",
"@internationalized/number": "3.6.5",
@ -85,7 +85,6 @@
"@prisma/client": "6.19.0",
"@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0",
"ai": "4.3.16",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
@ -98,7 +97,7 @@
"chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"class-validator": "0.14.3",
"color": "5.0.3",
"countries-and-timezones": "3.8.0",
"countries-list": "3.2.0",
@ -114,13 +113,12 @@
"ionicons": "8.0.13",
"jsonpath": "1.1.1",
"lodash": "4.17.21",
"marked": "15.0.4",
"marked": "17.0.1",
"ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "3.1.0",
"ngx-device-detector": "10.1.0",
"ngx-markdown": "20.0.0",
"ng-extract-i18n-merge": "3.2.1",
"ngx-device-detector": "11.0.0",
"ngx-markdown": "21.0.1",
"ngx-skeleton-loader": "11.3.0",
"ngx-stripe": "20.7.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
"passport": "0.7.0",
@ -130,46 +128,47 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "18.5.0",
"stripe": "20.1.0",
"svgmap": "2.14.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.27.0",
"yahoo-finance2": "3.10.2",
"zone.js": "0.15.1"
"yahoo-finance2": "3.11.2",
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "20.2.2",
"@angular-devkit/core": "20.2.2",
"@angular-devkit/schematics": "20.2.2",
"@angular-eslint/eslint-plugin": "20.2.0",
"@angular-eslint/eslint-plugin-template": "20.2.0",
"@angular-eslint/template-parser": "20.2.0",
"@angular/cli": "20.2.2",
"@angular/compiler-cli": "20.2.4",
"@angular/language-service": "20.2.4",
"@angular/localize": "20.2.4",
"@angular/pwa": "20.2.2",
"@angular-devkit/build-angular": "21.0.4",
"@angular-devkit/core": "21.0.4",
"@angular-devkit/schematics": "21.0.4",
"@angular-eslint/eslint-plugin": "21.1.0",
"@angular-eslint/eslint-plugin-template": "21.1.0",
"@angular-eslint/template-parser": "21.1.0",
"@angular/cli": "21.0.4",
"@angular/compiler-cli": "21.0.6",
"@angular/language-service": "21.0.6",
"@angular/localize": "21.0.6",
"@angular/pwa": "21.0.4",
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0",
"@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.8",
"@nx/angular": "21.5.1",
"@nx/eslint-plugin": "21.5.1",
"@nx/jest": "21.5.1",
"@nx/js": "21.5.1",
"@nx/module-federation": "21.5.1",
"@nx/nest": "21.5.1",
"@nx/node": "21.5.1",
"@nx/storybook": "21.5.1",
"@nx/web": "21.5.1",
"@nx/workspace": "21.5.1",
"@schematics/angular": "20.2.2",
"@storybook/addon-docs": "9.1.5",
"@storybook/angular": "9.1.5",
"@nx/angular": "22.3.3",
"@nx/eslint-plugin": "22.3.3",
"@nx/jest": "22.3.3",
"@nx/js": "22.3.3",
"@nx/module-federation": "22.3.3",
"@nx/nest": "22.3.3",
"@nx/node": "22.3.3",
"@nx/storybook": "22.3.3",
"@nx/web": "22.3.3",
"@nx/workspace": "22.3.3",
"@schematics/angular": "21.0.4",
"@storybook/addon-docs": "10.1.10",
"@storybook/angular": "10.1.10",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13",
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
"@types/lodash": "4.17.20",
"@types/node": "22.15.17",
"@types/papaparse": "5.3.7",
@ -180,12 +179,12 @@
"eslint": "9.35.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-storybook": "9.1.5",
"eslint-plugin-storybook": "10.1.10",
"husky": "9.1.7",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.6.0",
"nx": "21.5.1",
"jest": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0",
"nx": "22.3.3",
"prettier": "3.7.4",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.19.0",
@ -193,7 +192,7 @@
"react-dom": "18.2.0",
"replace-in-file": "8.3.0",
"shx": "0.4.0",
"storybook": "9.1.5",
"storybook": "10.1.10",
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"tslib": "2.8.1",

3
tsconfig.base.json

@ -4,8 +4,9 @@
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"moduleResolution": "bundler",
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2015",

Loading…
Cancel
Save