Browse Source

Merge remote-tracking branch 'origin/main' into feature/enable-strict-null-checks-in-ui

pull/6264/head
KenTandrian 5 days ago
parent
commit
42b3ab98ba
  1. 12
      CHANGELOG.md
  2. 6
      apps/api/src/app/account/account.controller.ts
  3. 2
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 2
      apps/api/src/app/subscription/subscription.service.ts
  5. 91
      apps/api/src/helper/object.helper.spec.ts
  6. 69
      apps/api/src/helper/object.helper.ts
  7. 42
      apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts
  8. 19
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  9. 58
      apps/client/src/app/components/admin-users/admin-users.component.ts
  10. 5
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  11. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  12. 23
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  13. 1
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  14. 25
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  15. 40
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  16. 10
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 2
      apps/client/src/app/pages/accounts/accounts-page.html
  18. 8
      libs/common/src/lib/chart-helper.ts
  19. 52
      libs/common/src/lib/config.ts
  20. 4
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  21. 14
      libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts
  22. 8
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  23. 5
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  24. 23
      libs/ui/src/lib/line-chart/line-chart.component.ts
  25. 31
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  26. 19
      libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts
  27. 30
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  28. 41
      package-lock.json
  29. 8
      package.json

12
CHANGELOG.md

@ -7,14 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Upgraded `stripe` from version `20.1.0` to `20.3.0`
## 2.235.0 - 2026-02-03
### Added
- Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_
- Added support for the impersonation mode in the endpoint `GET api/v1/account/:id/balances`
- Added an action menu to the user detail dialog in the users section of the admin control panel
### Changed
- Optimized the value redaction interceptor for the impersonation mode by introducing `fast-redact`
- Refactored `showTransactions` in favor of `showActivitiesCount` in the accounts table component
- Refactored `transactionCount` in favor of `activitiesCount` in the accounts table component
- Deprecated `transactionCount` in favor of `activitiesCount` in the endpoint `GET api/v1/admin`
- Removed the deprecated `firstBuyDate` in the portfolio calculator
- Upgraded `yahoo-finance2` from version `3.11.2` to `3.13.0`
## 2.234.0 - 2026-01-30

6
apps/api/src/app/account/account.controller.ts

@ -132,12 +132,16 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string
): Promise<AccountBalancesResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
return this.accountBalanceService.getAccountBalances({
filters: [{ id, type: 'ACCOUNT' }],
userCurrency: this.request.user.settings.settings.baseCurrency,
userId: this.request.user.id
userId: impersonationUserId || this.request.user.id
});
}

2
apps/api/src/app/portfolio/portfolio.controller.ts

@ -195,11 +195,13 @@ export class PortfolioController {
'excludedAccountsAndActivities',
'fees',
'filteredValueInBaseCurrency',
'fireWealth',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interestInBaseCurrency',
'items',
'liabilities',
'liabilitiesInBaseCurrency',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'totalBuy',

2
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-12-15.clover'
apiVersion: '2026-01-28.clover'
}
);
}

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

@ -1,4 +1,6 @@
import { query, redactAttributes } from './object.helper';
import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config';
import { query, redactPaths } from './object.helper';
describe('query', () => {
it('should get market price from stock API response', () => {
@ -22,46 +24,38 @@ describe('query', () => {
describe('redactAttributes', () => {
it('should redact provided attributes', () => {
expect(redactAttributes({ object: {}, options: [] })).toStrictEqual({});
expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({});
expect(
redactAttributes({ object: { value: 1000 }, options: [] })
).toStrictEqual({ value: 1000 });
expect(redactPaths({ object: { value: 1000 }, paths: [] })).toStrictEqual({
value: 1000
});
expect(
redactAttributes({
redactPaths({
object: { value: 1000 },
options: [{ attribute: 'value', valueMap: { '*': null } }]
paths: ['value']
})
).toStrictEqual({ value: null });
expect(
redactAttributes({
redactPaths({
object: { value: 'abc' },
options: [{ attribute: 'value', valueMap: { abc: 'xyz' } }]
paths: ['value'],
valueMap: { abc: 'xyz' }
})
).toStrictEqual({ value: 'xyz' });
expect(
redactAttributes({
redactPaths({
object: { data: [{ value: 'a' }, { value: 'b' }] },
options: [{ attribute: 'value', valueMap: { a: 1, b: 2 } }]
paths: ['data[*].value'],
valueMap: { a: 1, b: 2 }
})
).toStrictEqual({ data: [{ value: 1 }, { value: 2 }] });
expect(
redactAttributes({
object: { value1: 'a', value2: 'b' },
options: [
{ attribute: 'value1', valueMap: { a: 'x' } },
{ attribute: 'value2', valueMap: { '*': 'y' } }
]
})
).toStrictEqual({ value1: 'x', value2: 'y' });
console.time('redactAttributes execution time');
expect(
redactAttributes({
redactPaths({
object: {
accounts: {
'2e937c05-657c-4de9-8fb3-0813a2245f26': {
@ -1564,34 +1558,7 @@ describe('redactAttributes', () => {
currentNetWorth: null
}
},
options: [
'balance',
'balanceInBaseCurrency',
'comment',
'convertedBalance',
'dividendInBaseCurrency',
'fee',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'investment',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'quantity',
'symbolMapping',
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency',
'unitPrice',
'value',
'valueInBaseCurrency'
].map((attribute) => {
return {
attribute,
valueMap: {
'*': null
}
};
})
paths: DEFAULT_REDACTED_PATHS
})
).toStrictEqual({
accounts: {
@ -1681,7 +1648,7 @@ describe('redactAttributes', () => {
],
dataSource: 'EOD_HISTORICAL_DATA',
dateOfFirstActivity: '2021-11-30T23:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.3183066634822068,
grossPerformancePercentWithCurrencyEffect: 0.3183066634822068,
@ -1728,7 +1695,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-04-22T22:00:00.000Z',
dividend: 192,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.3719230057375532,
grossPerformancePercentWithCurrencyEffect: 0.2650716044872953,
@ -1780,7 +1747,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2018-09-30T22:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.8594552890963852,
grossPerformancePercentWithCurrencyEffect: 0.8594552890963852,
@ -1831,7 +1798,7 @@ describe('redactAttributes', () => {
countries: [],
dataSource: 'COINGECKO',
dateOfFirstActivity: '2017-08-15T22:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 17.4925166352,
grossPerformancePercentWithCurrencyEffect: 17.4925166352,
@ -1882,7 +1849,7 @@ describe('redactAttributes', () => {
countries: [],
dataSource: 'MANUAL',
dateOfFirstActivity: '2021-01-31T23:00:00.000Z',
dividend: 11.45,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: -0.06153834320225245,
@ -1986,7 +1953,7 @@ describe('redactAttributes', () => {
],
dataSource: 'MANUAL',
dateOfFirstActivity: '2021-03-31T22:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.27579517683678895,
grossPerformancePercentWithCurrencyEffect: 0.458553421589667,
@ -2038,7 +2005,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2023-01-02T23:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.7865431171216295,
grossPerformancePercentWithCurrencyEffect: 0.7865431171216295,
@ -2090,7 +2057,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2017-01-02T23:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 17.184314638161936,
grossPerformancePercentWithCurrencyEffect: 17.184314638161936,
@ -2172,7 +2139,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2019-02-28T23:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.8832083851170418,
grossPerformancePercentWithCurrencyEffect: 0.8832083851170418,
@ -2567,7 +2534,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2018-02-28T23:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.3683200415015591,
grossPerformancePercentWithCurrencyEffect: 0.5806366182968891,
@ -2846,7 +2813,7 @@ describe('redactAttributes', () => {
],
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-08-18T22:00:00.000Z',
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0.3474381850624522,
grossPerformancePercentWithCurrencyEffect: 0.28744846894552306,
@ -2964,7 +2931,7 @@ describe('redactAttributes', () => {
assetClass: 'LIQUIDITY',
assetSubClass: 'CASH',
countries: [],
dividend: 0,
dividend: null,
grossPerformance: null,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,

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

@ -1,6 +1,6 @@
import { Big } from 'big.js';
import fastRedact from 'fast-redact';
import jsonpath from 'jsonpath';
import { cloneDeep, isArray, isObject } from 'lodash';
import { cloneDeep, isObject } from 'lodash';
export function hasNotDefinedValuesInObject(aObject: Object): boolean {
for (const key in aObject) {
@ -42,60 +42,29 @@ export function query({
return jsonpath.query(object, pathExpression);
}
export function redactAttributes({
isFirstRun = true,
export function redactPaths({
object,
options
paths,
valueMap
}: {
isFirstRun?: boolean;
object: any;
options: { attribute: string; valueMap: { [key: string]: any } }[];
paths: fastRedact.RedactOptions['paths'];
valueMap?: { [key: string]: any };
}): any {
if (!object || !options?.length) {
return object;
}
// Create deep clone
const redactedObject = isFirstRun
? JSON.parse(JSON.stringify(object))
: object;
for (const option of options) {
if (redactedObject.hasOwnProperty(option.attribute)) {
if (option.valueMap['*'] || option.valueMap['*'] === null) {
redactedObject[option.attribute] = option.valueMap['*'];
} else if (option.valueMap[redactedObject[option.attribute]]) {
redactedObject[option.attribute] =
option.valueMap[redactedObject[option.attribute]];
}
} else {
// If the attribute is not present on the current object,
// check if it exists on any nested objects
for (const property in redactedObject) {
if (isArray(redactedObject[property])) {
redactedObject[property] = redactedObject[property].map(
(currentObject) => {
return redactAttributes({
options,
isFirstRun: false,
object: currentObject
});
}
);
} else if (
isObject(redactedObject[property]) &&
!(redactedObject[property] instanceof Big)
) {
// Recursively call the function on the nested object
redactedObject[property] = redactAttributes({
options,
isFirstRun: false,
object: redactedObject[property]
});
const redact = fastRedact({
paths,
censor: (value) => {
if (valueMap) {
if (valueMap[value]) {
return valueMap[value];
} else {
return value;
}
} else {
return null;
}
}
}
});
return redactedObject;
return JSON.parse(redact(object));
}

42
apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts

@ -1,5 +1,8 @@
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import {
DEFAULT_REDACTED_PATHS,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import {
hasReadRestrictedAccessPermission,
isRestrictedView
@ -39,40 +42,9 @@ export class RedactValuesInResponseInterceptor<T> implements NestInterceptor<
}) ||
isRestrictedView(user)
) {
data = redactAttributes({
data = redactPaths({
object: data,
options: [
'balance',
'balanceInBaseCurrency',
'comment',
'convertedBalance',
'dividendInBaseCurrency',
'fee',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interestInBaseCurrency',
'investment',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'quantity',
'symbolMapping',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency',
'totalValueInBaseCurrency',
'unitPrice',
'unitPriceInAssetProfileCurrency',
'value',
'valueInBaseCurrency'
].map((attribute) => {
return {
attribute,
valueMap: {
'*': null
}
};
})
paths: DEFAULT_REDACTED_PATHS
});
}

19
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -1,4 +1,4 @@
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { encodeDataSource } from '@ghostfolio/common/helper';
@ -58,13 +58,18 @@ export class TransformDataSourceInResponseInterceptor<
}
}
data = redactAttributes({
data = redactPaths({
valueMap,
object: data,
options: [
{
valueMap,
attribute: 'dataSource'
}
paths: [
'activities[*].SymbolProfile.dataSource',
'benchmarks[*].dataSource',
'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource',
'fearAndGreedIndex.STOCKS.dataSource',
'holdings[*].dataSource',
'items[*].dataSource',
'SymbolProfile.dataSource',
'watchlist[*].dataSource'
]
});
}

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

@ -57,7 +57,7 @@ import {
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { switchMap, takeUntil, tap } from 'rxjs/operators';
@Component({
imports: [
@ -139,8 +139,25 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
];
}
this.route.paramMap
.pipe(takeUntil(this.unsubscribeSubject))
this.userService.stateChanged
.pipe(
takeUntil(this.unsubscribeSubject),
tap((state) => {
if (state?.user) {
this.user = state.user;
this.defaultDateFormat = getDateFormatString(
this.user.settings.locale
);
this.hasPermissionToImpersonateAllUsers = hasPermission(
this.user.permissions,
permissions.impersonateAllUsers
);
}
}),
switchMap(() => this.route.paramMap)
)
.subscribe((params) => {
const userId = params.get('userId');
@ -149,23 +166,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
}
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.defaultDateFormat = getDateFormatString(
this.user.settings.locale
);
this.hasPermissionToImpersonateAllUsers = hasPermission(
this.user.permissions,
permissions.impersonateAllUsers
);
}
});
addIcons({
contractOutline,
ellipsisHorizontal,
@ -208,10 +208,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
.deleteUser(aId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.fetchUsers();
this.router.navigate(['..'], { relativeTo: this.route });
});
},
confirmType: ConfirmationDialogType.Warn,
discardFn: () => {
this.router.navigate(['..'], { relativeTo: this.route });
},
title: $localize`Do you really want to delete this user?`
});
}
@ -293,6 +296,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
>(GfUserDetailDialogComponent, {
autoFocus: false,
data: {
currentUserId: this.user?.id,
deviceType: this.deviceType,
hasPermissionForSubscription: this.hasPermissionForSubscription,
locale: this.user?.settings?.locale,
@ -305,10 +309,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(
internalRoutes.adminControl.subRoutes.users.routerLink
);
.subscribe((data) => {
if (data?.action === 'delete' && data?.userId) {
this.onDeleteUser(data.userId);
} else {
this.router.navigate(
internalRoutes.adminControl.subRoutes.users.routerLink
);
}
});
}
}

5
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -14,6 +14,7 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ColorScheme } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
@ -96,6 +97,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
Tooltip
);
registerChartConfiguration();
addIcons({ arrowForwardOutline });
}
@ -154,8 +157,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
if (this.chartCanvas) {
if (this.chart) {
this.chart.data = data;
this.chart.options.plugins ??= {};
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration();
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -380,10 +380,10 @@
[deviceType]="data.deviceType"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActivitiesCount]="false"
[showAllocationInPercentage]="user?.settings?.isExperimentalFeatures"
[showBalance]="false"
[showFooter]="false"
[showTransactions]="false"
[showValue]="false"
[showValueInBaseCurrency]="false"
/>

23
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -14,6 +14,7 @@ import {
import { LineChartItem } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart';
import { CommonModule } from '@angular/common';
import {
@ -34,12 +35,15 @@ import {
LineController,
LineElement,
PointElement,
type ScriptableLineSegmentContext,
TimeScale,
Tooltip,
type TooltipOptions
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import annotationPlugin, {
type AnnotationOptions
} from 'chartjs-plugin-annotation';
import { isAfter } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -80,6 +84,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
TimeScale,
Tooltip
);
registerChartConfiguration();
}
public ngOnChanges() {
@ -154,17 +160,14 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
if (this.chartCanvas) {
if (this.chart) {
this.chart.data = chartData;
this.chart.options.plugins ??= {};
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration();
if (
this.savingsRate &&
// @ts-ignore
this.chart.options.plugins.annotation.annotations.savingsRate
) {
// @ts-ignore
this.chart.options.plugins.annotation.annotations.savingsRate.value =
this.savingsRate;
const annotations = this.chart.options.plugins.annotation
.annotations as Record<string, AnnotationOptions<'line'>>;
if (this.savingsRate && annotations.savingsRate) {
annotations.savingsRate.value = this.savingsRate;
}
this.chart.update();
@ -301,7 +304,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
};
}
private isInFuture<T>(aContext: any, aValue: T) {
private isInFuture<T>(aContext: ScriptableLineSegmentContext, aValue: T) {
return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
? aValue
: undefined;

1
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -1,4 +1,5 @@
export interface UserDetailDialogParams {
currentUserId: string;
deviceType: string;
hasPermissionForSubscription: boolean;
locale: string;

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

@ -1,6 +1,4 @@
import { AdminUserResponse } from '@ghostfolio/common/interfaces';
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
import { AdminService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -16,6 +14,10 @@ import {
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { ellipsisVertical } from 'ionicons/icons';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
@ -25,11 +27,11 @@ import { UserDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfValueComponent,
IonIcon,
MatButtonModule,
MatDialogModule
MatDialogModule,
MatMenuModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-detail-dialog',
@ -46,7 +48,11 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {}
) {
addIcons({
ellipsisVertical
});
}
public ngOnInit() {
this.adminService
@ -66,6 +72,13 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
});
}
public deleteUser() {
this.dialogRef.close({
action: 'delete',
userId: this.data.userId
});
}
public onClose() {
this.dialogRef.close();
}

40
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -1,9 +1,28 @@
<gf-dialog-header
position="center"
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
<div class="d-flex justify-content-end">
<button
class="mx-1 no-min-width px-2"
mat-button
type="button"
[matMenuTriggerFor]="userDetailActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical" />
</button>
<mat-menu
#userDetailActionsMenu="matMenu"
class="no-max-width"
xPosition="before"
>
<button
mat-menu-item
type="button"
[disabled]="this.data.currentUserId === this.data.userId"
(click)="deleteUser()"
>
<ng-container i18n>Delete</ng-container>
</button>
</mat-menu>
</div>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="mb-3 row">
@ -103,7 +122,8 @@
</div>
</div>
<gf-dialog-footer
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
<div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onClose()">
<ng-container i18n>Close</ng-container>
</button>
</div>

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

@ -38,6 +38,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
})
export class GfAccountsPageComponent implements OnDestroy, OnInit {
public accounts: AccountModel[];
public activitiesCount = 0;
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionToCreateAccount: boolean;
@ -45,7 +46,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public routeQueryParams: Subscription;
public totalBalanceInBaseCurrency = 0;
public totalValueInBaseCurrency = 0;
public transactionCount = 0;
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -128,14 +128,14 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
.subscribe(
({
accounts,
activitiesCount,
totalBalanceInBaseCurrency,
totalValueInBaseCurrency,
transactionCount
totalValueInBaseCurrency
}) => {
this.accounts = accounts;
this.activitiesCount = activitiesCount;
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
this.transactionCount = transactionCount;
if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
@ -358,8 +358,8 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
private reset() {
this.accounts = undefined;
this.activitiesCount = 0;
this.totalBalanceInBaseCurrency = 0;
this.totalValueInBaseCurrency = 0;
this.transactionCount = 0;
}
}

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

@ -4,6 +4,7 @@
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
<gf-accounts-table
[accounts]="accounts"
[activitiesCount]="activitiesCount"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
@ -14,7 +15,6 @@
"
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
(transferBalance)="onTransferBalance()"

8
libs/common/src/lib/chart-helper.ts

@ -54,15 +54,17 @@ export function getTooltipOptions<T extends ChartType>({
bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`,
// @ts-expect-error: no need to set all attributes in callbacks.
// @ts-expect-error: no need to set all attributes in callbacks
callbacks: {
label: (context) => {
let label = (context.dataset as ControllerDatasetOptions).label ?? '';
if (label) {
label += ': ';
}
const yPoint = (context.parsed as Point).y;
if (yPoint !== null) {
if (currency) {
label += `${yPoint.toLocaleString(locale, {
@ -75,10 +77,12 @@ export function getTooltipOptions<T extends ChartType>({
label += yPoint.toFixed(2);
}
}
return label;
},
title: (contexts) => {
const xPoint = (contexts[0].parsed as Point).x;
if (groupBy && xPoint !== null) {
return formatGroupedDate({ groupBy, date: xPoint });
}
@ -105,6 +109,7 @@ export function getTooltipPositionerMapTop(
if (!position || !chart?.chartArea) {
return false;
}
return {
x: position.x,
y: chart.chartArea.top
@ -132,6 +137,7 @@ export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
const xValue = active[0].element.x;
const context = chartCanvas.nativeElement.getContext('2d');
if (context) {
context.lineWidth = width;
context.strokeStyle = color;

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

@ -78,6 +78,58 @@ export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
export const DEFAULT_REDACTED_PATHS = [
'accounts[*].balance',
'accounts[*].valueInBaseCurrency',
'activities[*].account.balance',
'activities[*].account.comment',
'activities[*].comment',
'activities[*].fee',
'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency',
'activities[*].quantity',
'activities[*].SymbolProfile.symbolMapping',
'activities[*].SymbolProfile.watchedByCount',
'activities[*].value',
'activities[*].valueInBaseCurrency',
'balance',
'balanceInBaseCurrency',
'balances[*].account.balance',
'balances[*].account.comment',
'balances[*].value',
'balances[*].valueInBaseCurrency',
'comment',
'dividendInBaseCurrency',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'historicalData[*].quantity',
'holdings[*].dividend',
'holdings[*].grossPerformance',
'holdings[*].grossPerformanceWithCurrencyEffect',
'holdings[*].holdings[*].valueInBaseCurrency',
'holdings[*].investment',
'holdings[*].netPerformance',
'holdings[*].netPerformanceWithCurrencyEffect',
'holdings[*].quantity',
'holdings[*].valueInBaseCurrency',
'interestInBaseCurrency',
'investmentInBaseCurrencyWithCurrencyEffect',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'platforms[*].balance',
'platforms[*].valueInBaseCurrency',
'quantity',
'SymbolProfile.symbolMapping',
'SymbolProfile.watchedByCount',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency',
'totalValueInBaseCurrency',
'value',
'valueInBaseCurrency'
];
// USX is handled separately
export const DERIVED_CURRENCIES = [
{

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

@ -115,7 +115,7 @@
></td>
</ng-container>
<ng-container matColumnDef="transactions">
<ng-container matColumnDef="activitiesCount">
<th
*matHeaderCellDef
class="justify-content-end px-1"
@ -129,7 +129,7 @@
{{ element.transactionCount }}
</td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
{{ transactionCount }}
{{ activitiesCount }}
</td>
</ng-container>

14
libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts

@ -115,10 +115,10 @@ export const Loading: Story = {
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showActivitiesCount: true,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true
}
@ -127,39 +127,39 @@ export const Loading: Story = {
export const Default: Story = {
args: {
accounts,
activitiesCount: 12,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showActivitiesCount: true,
showAllocationInPercentage: false,
showBalance: true,
showFooter: true,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 12428.2,
totalValueInBaseCurrency: 107971.70321466809,
transactionCount: 12
totalValueInBaseCurrency: 107971.70321466809
}
};
export const WithoutFooter: Story = {
args: {
accounts,
activitiesCount: 12,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
locale: 'en-US',
showActions: false,
showActivitiesCount: true,
showAllocationInPercentage: false,
showBalance: true,
showFooter: false,
showTransactions: true,
showValue: true,
showValueInBaseCurrency: true,
totalBalanceInBaseCurrency: 12428.2,
totalValueInBaseCurrency: 107971.70321466809,
transactionCount: 12
totalValueInBaseCurrency: 107971.70321466809
}
};

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

@ -55,20 +55,20 @@ import { Subject, Subscription } from 'rxjs';
})
export class GfAccountsTableComponent implements OnChanges, OnDestroy {
@Input() accounts: Account[];
@Input() activitiesCount: number;
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true;
@Input() locale = getLocale();
@Input() showActions: boolean;
@Input() showActivitiesCount = true;
@Input() showAllocationInPercentage: boolean;
@Input() showBalance = true;
@Input() showFooter = true;
@Input() showTransactions = true;
@Input() showValue = true;
@Input() showValueInBaseCurrency = true;
@Input() totalBalanceInBaseCurrency: number;
@Input() totalValueInBaseCurrency: number;
@Input() transactionCount: number;
@Output() accountDeleted = new EventEmitter<string>();
@Output() accountToUpdate = new EventEmitter<Account>();
@ -101,8 +101,8 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
public ngOnChanges() {
this.displayedColumns = ['status', 'account', 'platform'];
if (this.showTransactions) {
this.displayedColumns.push('transactions');
if (this.showActivitiesCount) {
this.displayedColumns.push('activitiesCount');
}
if (this.showBalance) {

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

@ -290,8 +290,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
callbacks: {
footer: (items) => {
const totalAmount = items.reduce(
// @ts-ignore
(a, b) => a + b.parsed.y,
(a, b) => a + (b.parsed.y ?? 0),
0
);
@ -313,8 +312,6 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
if (context.parsed.y !== null) {
label += new Intl.NumberFormat(this.locale, {
currency: this.currency,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Only supported from ES2020 or later
currencyDisplay: 'code',
style: 'currency'
}).format(context.parsed.y);

23
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -25,7 +25,7 @@ import {
ViewChild
} from '@angular/core';
import {
type AnimationSpec,
type AnimationsSpec,
Chart,
Filler,
LinearScale,
@ -39,6 +39,8 @@ import {
import 'chartjs-adapter-date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { registerChartConfiguration } from '../chart';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, NgxSkeletonLoaderModule],
@ -85,6 +87,8 @@ export class GfLineChartComponent
TimeScale,
Tooltip
);
registerChartConfiguration();
}
public ngAfterViewInit() {
@ -131,8 +135,10 @@ export class GfLineChartComponent
0,
0,
0,
// @ts-ignore
(this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5
((this.chartCanvas.nativeElement.parentNode as HTMLElement)
.offsetHeight *
4) /
5
);
if (gradient && this.showGradient) {
@ -175,12 +181,12 @@ export class GfLineChartComponent
if (this.chart) {
this.chart.data = data;
this.chart.options.animations = this.isAnimated
? animations
: undefined;
this.chart.options.plugins ??= {};
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration();
this.chart.options.animations = this.isAnimated
? animations
: undefined;
this.chart.update();
} else {
@ -296,7 +302,7 @@ export class GfLineChartComponent
}: {
axis: 'x' | 'y';
labels: string[];
}): AnimationSpec<'line'> {
}): Partial<AnimationsSpec<'line'>[string]> {
const delayBetweenPoints = this.ANIMATION_DURATION / labels.length;
return {
@ -306,8 +312,7 @@ export class GfLineChartComponent
}
context[`${axis}Started`] = true;
// @ts-ignore
return context.index * delayBetweenPoints;
return context.dataIndex * delayBetweenPoints;
},
duration: delayBetweenPoints,
easing: 'linear',

31
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -29,9 +29,7 @@ import {
type ChartDataset,
DoughnutController,
LinearScale,
type Plugin,
Tooltip,
type TooltipItem,
type TooltipOptions
} from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
@ -357,23 +355,22 @@ export class GfPortfolioProportionChartComponent
layout: {
padding: this.showLabels === true ? 100 : 0
},
onClick: (event, activeElements) => {
onClick: (_, activeElements, chart) => {
try {
const dataIndex = activeElements[0].index;
// @ts-ignore
const symbol: string = event.chart.data.labels[dataIndex];
const symbol = chart.data.labels?.[dataIndex] as string;
const dataSource = this.data[symbol].dataSource!;
const dataSource = this.data[symbol].dataSource;
this.proportionChartClicked.emit({ dataSource, symbol });
if (dataSource) {
this.proportionChartClicked.emit({ dataSource, symbol });
}
} catch {}
},
onHover: (event, chartElement) => {
if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
(event.native?.target as HTMLElement).style.cursor =
chartElement[0] ? this.cursor : 'default';
}
},
plugins: {
@ -407,7 +404,7 @@ export class GfPortfolioProportionChartComponent
tooltip: this.getTooltipPluginConfiguration(data)
}
},
plugins: [ChartDataLabels as Plugin<'doughnut'>],
plugins: [ChartDataLabels],
type: 'doughnut'
});
}
@ -442,13 +439,15 @@ export class GfPortfolioProportionChartComponent
currency: this.baseCurrency,
locale: this.locale
}),
// @ts-ignore
// @ts-expect-error: no need to set all attributes in callbacks
callbacks: {
label: (context: TooltipItem<'doughnut'>) => {
const labelIndex =
(data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) +
context.dataIndex;
let symbol = context.chart.data.labels?.[labelIndex] ?? '';
let symbol =
(context.chart.data.labels?.[labelIndex] as string) ?? '';
if (symbol === this.OTHER_KEY) {
symbol = $localize`Other`;
@ -456,9 +455,10 @@ export class GfPortfolioProportionChartComponent
symbol = $localize`No data available`;
}
const name = translate(this.data[symbol as string]?.name);
const name = translate(this.data[symbol]?.name);
let sum = 0;
for (const item of context.dataset.data) {
sum += item;
}
@ -471,6 +471,7 @@ export class GfPortfolioProportionChartComponent
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`];
} else {
const value = context.raw as number;
return [
`${name ?? symbol}`,
`${value.toLocaleString(this.locale, {

19
libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts

@ -1,9 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
TreemapDataPoint,
TreemapScriptableContext
} from 'chartjs-chart-treemap';
import { ScriptableContext, TooltipItem } from 'chart.js';
import { TreemapDataPoint } from 'chartjs-chart-treemap';
export interface GetColorParams {
annualizedNetPerformancePercent: number;
@ -11,8 +9,13 @@ export interface GetColorParams {
positiveNetPerformancePercentsRange: { max: number; min: number };
}
export interface GfTreemapChartTooltipContext extends TreemapScriptableContext {
raw: TreemapDataPoint & {
_data: PortfolioPosition;
};
interface GfTreemapDataPoint extends TreemapDataPoint {
_data: PortfolioPosition;
}
export interface GfTreemapScriptableContext extends ScriptableContext<'treemap'> {
raw: GfTreemapDataPoint;
}
export interface GfTreemapTooltipItem extends TooltipItem<'treemap'> {
raw: GfTreemapDataPoint;
}

30
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -35,9 +35,10 @@ import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import {
import type {
GetColorParams,
GfTreemapChartTooltipContext
GfTreemapScriptableContext,
GfTreemapTooltipItem
} from './interfaces/interfaces';
const { gray, green, red } = OpenColor;
@ -204,7 +205,7 @@ export class GfTreemapChartComponent
const data: ChartData<'treemap'> = {
datasets: [
{
backgroundColor: (context: GfTreemapChartTooltipContext) => {
backgroundColor: (context: GfTreemapScriptableContext) => {
let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
@ -235,7 +236,7 @@ export class GfTreemapChartComponent
key: 'allocationInPercentage',
labels: {
align: 'left',
color: (context: GfTreemapChartTooltipContext) => {
color: (context: GfTreemapScriptableContext) => {
let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
@ -264,7 +265,7 @@ export class GfTreemapChartComponent
},
display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: ({ raw }: GfTreemapChartTooltipContext) => {
formatter: ({ raw }: GfTreemapScriptableContext) => {
// Round to 4 decimal places
let netPerformancePercentWithCurrencyEffect =
Math.round(
@ -289,7 +290,7 @@ export class GfTreemapChartComponent
position: 'top'
},
spacing: 1,
// @ts-ignore
// @ts-expect-error: should be PortfolioPosition[]
tree: this.holdings
}
]
@ -308,17 +309,16 @@ export class GfTreemapChartComponent
data,
options: {
animation: false,
onClick: (event, activeElements) => {
onClick: (_, activeElements, chart: Chart<'treemap'>) => {
try {
const dataIndex = activeElements[0].index;
const datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy(
// @ts-ignore
event.chart.data.datasets[datasetIndex].tree,
chart.data.datasets[datasetIndex].tree,
['allocationInPercentage'],
['desc']
);
) as PortfolioPosition[];
const dataSource: DataSource = dataset[dataIndex].dataSource;
const symbol: string = dataset[dataIndex].symbol;
@ -328,10 +328,8 @@ export class GfTreemapChartComponent
},
onHover: (event, chartElement) => {
if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
(event.native?.target as HTMLElement).style.cursor =
chartElement[0] ? this.cursor : 'default';
}
},
plugins: {
@ -353,9 +351,9 @@ export class GfTreemapChartComponent
currency: this.baseCurrency,
locale: this.locale
}),
// @ts-expect-error: no need to set all attributes in callbacks
callbacks: {
// @ts-ignore
label: ({ raw }: GfTreemapChartTooltipContext) => {
label: ({ raw }: GfTreemapTooltipItem) => {
const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.name;
const sign =

41
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "2.234.0",
"version": "2.235.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.234.0",
"version": "2.235.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -62,6 +62,7 @@
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.1",
"fast-redact": "3.5.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -84,11 +85,11 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "20.1.0",
"stripe": "20.3.0",
"svgmap": "2.14.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.27.0",
"yahoo-finance2": "3.11.2",
"yahoo-finance2": "3.13.0",
"zone.js": "0.16.0"
},
"devDependencies": {
@ -122,6 +123,7 @@
"@storybook/angular": "10.1.10",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2",
"@types/fast-redact": "3.0.4",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
@ -12936,6 +12938,13 @@
"@types/send": "*"
}
},
"node_modules/@types/fast-redact": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/fast-redact/-/fast-redact-3.0.4.tgz",
"integrity": "sha512-tgGJaXucrCH4Yx2l/AI6e/JQksZhKGIQsVwBMTh+nxUhQDv5tXScTs5DHTw+qSKDXnHL2dTAh1e2rd5pcFQyNQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
@ -19940,6 +19949,15 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@ -32087,13 +32105,10 @@
}
},
"node_modules/stripe": {
"version": "20.1.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.0.tgz",
"integrity": "sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==",
"version": "20.3.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz",
"integrity": "sha512-DYzcmV1MfYhycr1GwjCjeQVYk9Gu8dpxyTlu7qeDCsuguug7oUTxPsUQuZeSf/OPzK7pofqobvOKVqAwlpgf/Q==",
"license": "MIT",
"dependencies": {
"qs": "^6.11.0"
},
"engines": {
"node": ">=16"
},
@ -35361,9 +35376,9 @@
}
},
"node_modules/yahoo-finance2": {
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.11.2.tgz",
"integrity": "sha512-SIvMXjrOktBRD8m+qXAGCK+vR1vwBKuMgCnvmbxv29+t6LTDu0vAUxNYfbigsMRTmBzS4F9TQwbYF90g3Om4HA==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.13.0.tgz",
"integrity": "sha512-czBj2q/MD68YEsB7aXNnGhJvWxYZn01O5r/i7VYiQV2m2sWwhca6tKgjwf/LT7zHHEVxhKNiGLB46glLnmq9Ag==",
"license": "MIT",
"dependencies": {
"@deno/shim-deno": "~0.18.0",

8
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.234.0",
"version": "2.235.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -106,6 +106,7 @@
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
"envalid": "8.1.1",
"fast-redact": "3.5.0",
"fuse.js": "7.1.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
@ -128,11 +129,11 @@
"passport-openidconnect": "0.1.2",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"stripe": "20.1.0",
"stripe": "20.3.0",
"svgmap": "2.14.0",
"tablemark": "4.1.0",
"twitter-api-v2": "1.27.0",
"yahoo-finance2": "3.11.2",
"yahoo-finance2": "3.13.0",
"zone.js": "0.16.0"
},
"devDependencies": {
@ -166,6 +167,7 @@
"@storybook/angular": "10.1.10",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2",
"@types/fast-redact": "3.0.4",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",

Loading…
Cancel
Save