Browse Source

Merge branch 'main' into feature/add-holdings-to-overview

pull/7861/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
68f2a1c369
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 6
      apps/api/src/app/endpoints/mcp/mcp.controller.spec.ts
  3. 24
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  4. 2
      apps/api/src/app/endpoints/mcp/mcp.module.ts
  5. 47
      apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts
  6. 15
      apps/api/src/app/endpoints/mcp/mcp.schemas.ts
  7. 87
      apps/api/src/app/endpoints/mcp/mcp.service.spec.ts
  8. 60
      apps/api/src/app/endpoints/mcp/mcp.service.ts
  9. 7
      apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
  10. 8
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  11. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  12. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  13. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  14. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  15. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  16. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  17. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  18. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-before-first-market-price.spec.ts
  19. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  20. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  21. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  22. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  23. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  24. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts
  25. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  26. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-before-first-market-price.spec.ts
  27. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  28. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts
  29. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  30. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-activities.spec.ts
  31. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  32. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  33. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts
  34. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  35. 3
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  36. 15
      apps/api/src/app/portfolio/current-rate.service.ts
  37. 2
      apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts
  38. 12
      apps/api/src/app/portfolio/portfolio.service.ts
  39. 8
      apps/api/src/services/data-provider/data-provider.service.ts
  40. 382
      apps/client/src/locales/messages.ca.xlf
  41. 382
      apps/client/src/locales/messages.de.xlf
  42. 382
      apps/client/src/locales/messages.es.xlf
  43. 382
      apps/client/src/locales/messages.fr.xlf
  44. 382
      apps/client/src/locales/messages.it.xlf
  45. 382
      apps/client/src/locales/messages.ja.xlf
  46. 382
      apps/client/src/locales/messages.ko.xlf
  47. 382
      apps/client/src/locales/messages.nl.xlf
  48. 382
      apps/client/src/locales/messages.pl.xlf
  49. 382
      apps/client/src/locales/messages.pt.xlf
  50. 382
      apps/client/src/locales/messages.tr.xlf
  51. 382
      apps/client/src/locales/messages.uk.xlf
  52. 381
      apps/client/src/locales/messages.xlf
  53. 586
      apps/client/src/locales/messages.zh.xlf
  54. 18
      libs/ui/src/lib/no-activities-info/no-activities-info.component.html
  55. 70
      package-lock.json
  56. 12
      package.json

8
CHANGELOG.md

@ -10,11 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the holdings table to the overview tab of the home page on desktop (experimental)
- Added a tool to search for asset profiles to the server of the Model Context Protocol (MCP) (experimental)
### Changed
- Improved the style of the activity type filter on the activities page (experimental)
- Improved the style of the no activities info component
- Improved the style of the selector in the benchmark comparator
- Improved the language localization for Chinese (`zh`)
- Improved the language localization for German (`de`)
- Upgraded `@codewithdan/observable-store` from version `2.2.15` to `3.0.0`
- Upgraded `bull-board` from version `9.5.0` to `9.8.0`
- Upgraded `papaparse` from version `5.5.3` to `5.7.0`
- Upgraded `zone.js` from version `0.16.2` to `0.16.3`
## 3.69.0 - 2026-09-07

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

@ -77,6 +77,12 @@ describe('GhostfolioMcpController', () => {
).toEqual([scopes.activityCreate]);
});
it('Requires the scope to create an activity for the tool to search asset profiles', () => {
expect(
getMetadataOfMethod<Scope[]>(REQUIRES_SCOPE_KEY, 'searchAssetProfiles')
).toEqual([scopes.activityCreate]);
});
// The tools have no try and catch, hence the filter is the only guarantee
// that an unexpected exception does not expose internals
it('Applies the filter of the exceptions of the tools', () => {

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

@ -15,7 +15,8 @@ import 'zod/compile';
import {
GET_ACCOUNTS_PARAMETERS,
GET_ACTIVITIES_PARAMETERS,
IMPORT_ACTIVITIES_PARAMETERS
IMPORT_ACTIVITIES_PARAMETERS,
SEARCH_ASSET_PROFILES_PARAMETERS
} from './mcp.schemas';
import { McpService } from './mcp.service';
@ -98,7 +99,7 @@ export class GhostfolioMcpController {
readOnlyHint: false,
title: 'Import activities'
},
description: `Imports activities into the portfolio and gives the number of the imported activities and the number of the skipped activities. An activity is skipped if an equal activity is in the portfolio already, hence send each activity one time only: two equal activities of the same call are both imported. The access needs the permission "Restricted view and manage". At most ${MCP_MAX_ACTIVITIES} activities are imported per call, while the instance can have a lower limit, which an error names. An error does not remove the activities of the same call which are imported already, hence get the activities after an error before you import them again.`,
description: `Imports activities into the portfolio and gives the number of the imported activities and the number of the skipped activities. Use search-asset-profiles first unless the exact symbol and data source are already known. An activity is skipped if an equal activity is in the portfolio already, hence send each activity one time only: two equal activities of the same call are both imported. The access needs the permission "Restricted view and manage". At most ${MCP_MAX_ACTIVITIES} activities are imported per call, while the instance can have a lower limit, which an error names. An error does not remove the activities of the same call which are imported already, hence get the activities after an error before you import them again.`,
name: 'import-activities',
parameters: IMPORT_ACTIVITIES_PARAMETERS
})
@ -108,4 +109,23 @@ export class GhostfolioMcpController {
) {
return this.mcpService.importActivities({ ...parameters, userId });
}
@RequiresScopeOfAccess(scopes.activityCreate)
@Tool({
annotations: {
openWorldHint: true,
readOnlyHint: true,
title: 'Search asset profiles'
},
description:
'Searches for financial assets, such as stocks, ETFs, cryptocurrencies, mutual funds and commodities, which are available to the user. Each result is an asset profile that can be used to import an activity. Use this before importing an activity unless the exact symbol and data source are already known. Select the candidate that matches the intended asset and pass its symbol, dataSource and currency unchanged to import-activities.',
name: 'search-asset-profiles',
parameters: SEARCH_ASSET_PROFILES_PARAMETERS
})
public async searchAssetProfiles(
@Impersonation() { userId }: ImpersonationContext,
@Payload() parameters: z.infer<typeof SEARCH_ASSET_PROFILES_PARAMETERS>
) {
return this.mcpService.searchAssetProfiles({ ...parameters, userId });
}
}

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

@ -1,4 +1,5 @@
import { ImportModule } from '@ghostfolio/api/app/import/import.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { environment } from '@ghostfolio/api/environments/environment';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
@ -24,6 +25,7 @@ import { McpService } from './mcp.service';
ConfigurationModule,
ImportModule,
PortfolioTableModule,
SymbolModule,
UserModule
],
providers: [

47
apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts

@ -1,6 +1,13 @@
import { MCP_MAX_ACTIVITIES } from '@ghostfolio/common/config';
import {
MCP_MAX_ACTIVITIES,
SEARCH_QUERY_MAXIMUM_LENGTH,
SEARCH_QUERY_MINIMUM_LENGTH
} from '@ghostfolio/common/config';
import { IMPORT_ACTIVITIES_PARAMETERS } from './mcp.schemas';
import {
IMPORT_ACTIVITIES_PARAMETERS,
SEARCH_ASSET_PROFILES_PARAMETERS
} from './mcp.schemas';
import { createActivity } from './mcp.test-utils';
describe('IMPORT_ACTIVITIES_PARAMETERS', () => {
@ -46,3 +53,39 @@ describe('IMPORT_ACTIVITIES_PARAMETERS', () => {
).toBe(false);
});
});
describe('SEARCH_ASSET_PROFILES_PARAMETERS', () => {
it('Refuses a query that contains only spaces', () => {
expect(
SEARCH_ASSET_PROFILES_PARAMETERS.safeParse({ query: ' ' }).success
).toBe(false);
});
it(`Refuses a query shorter than ${SEARCH_QUERY_MINIMUM_LENGTH} characters`, () => {
expect(
SEARCH_ASSET_PROFILES_PARAMETERS.safeParse({ query: 'A' }).success
).toBe(false);
});
it(`Refuses a query longer than ${SEARCH_QUERY_MAXIMUM_LENGTH} characters`, () => {
expect(
SEARCH_ASSET_PROFILES_PARAMETERS.safeParse({
query: 'A'.repeat(SEARCH_QUERY_MAXIMUM_LENGTH + 1)
}).success
).toBe(false);
});
it('Accepts a name, symbol or ISIN', () => {
for (const query of ['Apple', 'AAPL', 'US0378331005']) {
expect(
SEARCH_ASSET_PROFILES_PARAMETERS.safeParse({ query }).success
).toBe(true);
}
});
it('Removes spaces at the start and the end of a query', () => {
expect(
SEARCH_ASSET_PROFILES_PARAMETERS.parse({ query: ' Apple ' }).query
).toBe('Apple');
});
});

15
apps/api/src/app/endpoints/mcp/mcp.schemas.ts

@ -2,7 +2,9 @@ import { DATE_RANGE_PATTERN } from '@ghostfolio/api/dtos/date-range-filter.dto';
import {
DATE_RANGES,
MCP_MAX_ACCOUNTS,
MCP_MAX_ACTIVITIES
MCP_MAX_ACTIVITIES,
SEARCH_QUERY_MAXIMUM_LENGTH,
SEARCH_QUERY_MINIMUM_LENGTH
} from '@ghostfolio/common/config';
import {
isValidCurrencyCode,
@ -113,3 +115,14 @@ export const IMPORT_ACTIVITIES_PARAMETERS = z.object({
.max(MCP_MAX_ACTIVITIES)
.describe(`The activities to import, at most ${MCP_MAX_ACTIVITIES}`)
});
export const SEARCH_ASSET_PROFILES_PARAMETERS = z.object({
query: z
.string()
.trim()
.min(SEARCH_QUERY_MINIMUM_LENGTH)
.max(SEARCH_QUERY_MAXIMUM_LENGTH)
.describe(
'The name, ticker symbol or ISIN of the financial asset, for example Apple, AAPL, Bitcoin or US0378331005'
)
});

87
apps/api/src/app/endpoints/mcp/mcp.service.spec.ts

@ -1,5 +1,6 @@
import { ImportValidationError } from '@ghostfolio/api/app/import/errors/import-validation.error';
import { ImportService } from '@ghostfolio/api/app/import/import.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
@ -13,7 +14,12 @@ import { permissions } from '@ghostfolio/common/permissions';
import type { UserWithSettings } from '@ghostfolio/common/types';
import { HttpException } from '@nestjs/common';
import { AssetClass, DataSource, Type as ActivityType } from '@prisma/client';
import {
AssetClass,
AssetSubClass,
DataSource,
Type as ActivityType
} from '@prisma/client';
import { McpService } from './mcp.service';
import { createActivity } from './mcp.test-utils';
@ -29,12 +35,18 @@ describe('McpService', () => {
let importService: ImportService;
let mcpService: McpService;
let portfolioTableService: PortfolioTableService;
let symbolService: SymbolService;
let userService: UserService;
function setupUser(userPermissions: string[]) {
jest.spyOn(userService, 'user').mockResolvedValue({
const user = {
id: userId,
permissions: userPermissions
} as UserWithSettings);
} as UserWithSettings;
jest.spyOn(userService, 'user').mockResolvedValue(user);
return user;
}
beforeEach(() => {
@ -63,6 +75,10 @@ describe('McpService', () => {
getHoldingsTable: jest.fn().mockResolvedValue('## Holdings')
} as unknown as PortfolioTableService;
symbolService = {
lookup: jest.fn().mockResolvedValue({ items: [] })
} as unknown as SymbolService;
userService = { user: jest.fn() } as unknown as UserService;
mcpService = new McpService(
@ -70,6 +86,7 @@ describe('McpService', () => {
configurationService,
importService,
portfolioTableService,
symbolService,
userService
);
});
@ -182,6 +199,70 @@ describe('McpService', () => {
});
});
describe('searchAssetProfiles', () => {
it('Refuses a user without the permission to create an activity', async () => {
setupUser([]);
await expect(
mcpService.searchAssetProfiles({ query: 'Apple', userId })
).rejects.toThrow(HttpException);
expect(symbolService.lookup).not.toHaveBeenCalled();
});
it('Gives the import-ready asset profiles available to the user', async () => {
const user = setupUser([permissions.createActivity]);
configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO];
configuration.ENABLE_FEATURE_SUBSCRIPTION = true;
jest.spyOn(symbolService, 'lookup').mockResolvedValue({
items: [
{
assetClass: AssetClass.EQUITY,
assetSubClass: AssetSubClass.STOCK,
currency: 'USD',
dataProviderInfo: { isPremium: false },
dataSource: DataSource.YAHOO,
name: 'Apple Inc.',
symbol: 'AAPL'
},
{
assetClass: AssetClass.EQUITY,
assetSubClass: AssetSubClass.STOCK,
currency: 'USD',
dataProviderInfo: { isPremium: true },
dataSource: DataSource.GHOSTFOLIO,
name: 'Premium asset',
symbol: 'PREMIUM'
}
]
});
const result = await mcpService.searchAssetProfiles({
query: 'Apple',
userId
});
expect(symbolService.lookup).toHaveBeenCalledWith({
query: 'Apple',
user
});
expect(JSON.parse(result.content[0].text)).toEqual({
assetProfiles: [
{
assetClass: AssetClass.EQUITY,
assetSubClass: AssetSubClass.STOCK,
currency: 'USD',
dataSource: DataSource.GHOSTFOLIO,
name: 'Apple Inc.',
symbol: 'AAPL'
}
]
});
});
});
describe('importActivities', () => {
it('Refuses a user without the permission to create an activity', async () => {
setupUser([]);

60
apps/api/src/app/endpoints/mcp/mcp.service.ts

@ -1,6 +1,10 @@
import { ImportService } from '@ghostfolio/api/app/import/import.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { getUnmaskedGhostfolioDataSource } from '@ghostfolio/api/helper/data-source.helper';
import {
getMaskedGhostfolioDataSource,
getUnmaskedGhostfolioDataSource
} from '@ghostfolio/api/helper/data-source.helper';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PortfolioTableService } from '@ghostfolio/api/services/portfolio-table/portfolio-table.service';
@ -15,7 +19,8 @@ import { z } from 'zod';
import {
GET_ACCOUNTS_PARAMETERS,
GET_ACTIVITIES_PARAMETERS,
IMPORT_ACTIVITIES_PARAMETERS
IMPORT_ACTIVITIES_PARAMETERS,
SEARCH_ASSET_PROFILES_PARAMETERS
} from './mcp.schemas';
@Injectable()
@ -25,6 +30,7 @@ export class McpService {
private readonly configurationService: ConfigurationService,
private readonly importService: ImportService,
private readonly portfolioTableService: PortfolioTableService,
private readonly symbolService: SymbolService,
private readonly userService: UserService
) {}
@ -146,6 +152,56 @@ export class McpService {
return this.getTextResult(text);
}
public async searchAssetProfiles({
query,
userId
}: z.infer<typeof SEARCH_ASSET_PROFILES_PARAMETERS> & { userId: string }) {
const user = await this.getUserWithPermission({
userId,
permission: permissions.createActivity
});
const { items } = await this.symbolService.lookup({ query, user });
const ghostfolioDataSources = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
? this.configurationService.get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER')
: [];
const assetProfiles = items.flatMap(
({
assetClass,
assetSubClass,
currency,
dataProviderInfo,
dataSource,
name,
symbol
}) => {
if (!dataSource || dataProviderInfo.isPremium) {
return [];
}
return [
{
assetClass,
assetSubClass,
currency,
name,
symbol,
dataSource: getMaskedGhostfolioDataSource({
dataSource,
ghostfolioDataSources
})
}
];
}
);
return this.getTextResult(JSON.stringify({ assetProfiles }, null, 2));
}
private getTextResult(text: string) {
return { content: [{ text, type: 'text' as const }] };
}

7
apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts

@ -3,6 +3,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
Activity,
Filter,
@ -35,6 +36,7 @@ export class PortfolioCalculatorFactory {
currency,
filters = [],
usePortfolioSnapshotCache = true,
subscriptionType,
userId
}: {
accountBalanceItems?: HistoricalDataItem[];
@ -43,6 +45,7 @@ export class PortfolioCalculatorFactory {
currency: string;
filters?: Filter[];
usePortfolioSnapshotCache?: boolean;
subscriptionType?: SubscriptionType;
userId: string;
}): PortfolioCalculator {
switch (calculationType) {
@ -53,6 +56,7 @@ export class PortfolioCalculatorFactory {
currency,
filters,
usePortfolioSnapshotCache,
subscriptionType,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@ -68,6 +72,7 @@ export class PortfolioCalculatorFactory {
currency,
filters,
usePortfolioSnapshotCache,
subscriptionType,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@ -83,6 +88,7 @@ export class PortfolioCalculatorFactory {
currency,
filters,
usePortfolioSnapshotCache,
subscriptionType,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,
@ -98,6 +104,7 @@ export class PortfolioCalculatorFactory {
currency,
filters,
usePortfolioSnapshotCache,
subscriptionType,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService,

8
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -20,6 +20,7 @@ import {
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH,
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
@ -89,6 +90,7 @@ export abstract class PortfolioCalculator {
private snapshot: PortfolioSnapshot;
private snapshotPromise: Promise<void>;
private startDate: Date;
private subscriptionType?: SubscriptionType;
private transactionPoints: TransactionPoint[];
private usePortfolioSnapshotCache: boolean;
private userId: string;
@ -104,6 +106,7 @@ export abstract class PortfolioCalculator {
portfolioSnapshotService,
redisCacheService,
usePortfolioSnapshotCache = true,
subscriptionType,
userId
}: {
accountBalanceItems: HistoricalDataItem[];
@ -116,6 +119,7 @@ export abstract class PortfolioCalculator {
portfolioSnapshotService: PortfolioSnapshotService;
redisCacheService: RedisCacheService;
usePortfolioSnapshotCache?: boolean;
subscriptionType?: SubscriptionType;
userId: string;
}) {
this.accountBalanceItems = accountBalanceItems;
@ -179,6 +183,7 @@ export abstract class PortfolioCalculator {
this.portfolioSnapshotService = portfolioSnapshotService;
this.redisCacheService = redisCacheService;
this.usePortfolioSnapshotCache = usePortfolioSnapshotCache;
this.subscriptionType = subscriptionType;
this.userId = userId;
const { endDate, startDate } = getIntervalFromDateRange({
@ -286,7 +291,8 @@ export abstract class PortfolioCalculator {
dateQuery: {
gte: this.startDate,
lt: this.endDate
}
},
subscriptionType: this.subscriptionType
});
this.dataProviderInfos = dataProviderInfos;

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts

@ -81,7 +81,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -69,7 +69,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -71,7 +71,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-before-first-market-price.spec.ts

@ -57,7 +57,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -69,7 +69,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -69,7 +69,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -118,7 +118,6 @@ describe('PortfolioCalculator', () => {
currentRateService = new CurrentRateService(
null,
dataProviderService,
null,
null
);

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts

@ -71,7 +71,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts

@ -72,7 +72,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-before-first-market-price.spec.ts

@ -57,7 +57,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts

@ -56,7 +56,8 @@ describe('PortfolioCalculator', () => {
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts

@ -71,7 +71,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-activities.spec.ts

@ -54,7 +54,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -72,7 +72,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -72,7 +72,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

3
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts

@ -60,7 +60,8 @@ describe('RoaiPortfolioCalculator stock splits', () => {
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts

@ -59,7 +59,7 @@ describe('PortfolioCalculator', () => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,

3
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -119,8 +119,7 @@ describe('CurrentRateService', () => {
currentRateService = new CurrentRateService(
null,
dataProviderService,
marketDataService,
null
marketDataService
);
});

15
apps/api/src/app/portfolio/current-rate.service.ts

@ -11,10 +11,8 @@ import {
DataProviderInfo,
ResponseError
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash';
@ -29,15 +27,14 @@ export class CurrentRateService {
public constructor(
private readonly activitiesService: ActivitiesService,
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
private readonly marketDataService: MarketDataService
) {}
@LogPerformance
// TODO: Pass user instead of using this.request.user
public async getValues({
dataGatheringItems,
dateQuery
dateQuery,
subscriptionType
}: GetValuesParams): Promise<GetValuesObject> {
const dataProviderInfos: DataProviderInfo[] = [];
@ -52,8 +49,8 @@ export class CurrentRateService {
if (includesToday) {
const quotes = await this.dataProviderService.getQuotes({
items: dataGatheringItems,
user: this.request?.user
subscriptionType,
items: dataGatheringItems
});
for (const { dataSource, symbol } of dataGatheringItems) {

2
apps/api/src/app/portfolio/interfaces/get-values-params.interface.ts

@ -1,8 +1,10 @@
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { DateQuery } from './date-query.interface';
export interface GetValuesParams {
dataGatheringItems: DataGatheringItem[];
dateQuery: DateQuery;
subscriptionType?: SubscriptionType;
}

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

@ -546,7 +546,8 @@ export class PortfolioService {
filters,
userId,
calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency
currency: userCurrency,
subscriptionType: user.subscription?.type
});
const { historicalData } = await portfolioCalculator.getSnapshot();
@ -633,7 +634,8 @@ export class PortfolioService {
userId,
calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency,
filters: portfolioSnapshotFilters
filters: portfolioSnapshotFilters,
subscriptionType: user.subscription?.type
});
const { createdAt, currentValueInBaseCurrency, hasErrors, positions } =
@ -963,7 +965,8 @@ export class PortfolioService {
calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency,
filters: isExcludedHolding ? holdingFilters : undefined,
usePortfolioSnapshotCache: !isExcludedHolding
usePortfolioSnapshotCache: !isExcludedHolding,
subscriptionType: user.subscription?.type
});
const transactionPoints = portfolioCalculator.getTransactionPoints();
@ -1218,7 +1221,8 @@ export class PortfolioService {
filters,
userId,
calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency
currency: userCurrency,
subscriptionType: user.subscription?.type
});
const { errors, hasErrors, historicalData } =

8
apps/api/src/services/data-provider/data-provider.service.ts

@ -594,13 +594,13 @@ export class DataProviderService implements OnModuleInit {
public async getQuotes({
items,
requestTimeout,
useCache = true,
user
subscriptionType,
useCache = true
}: {
items: AssetProfileIdentifier[];
requestTimeout?: number;
subscriptionType?: SubscriptionType;
useCache?: boolean;
user?: UserWithSettings;
}): Promise<{
[assetProfileIdentifier: string]: DataProviderResponse;
}> {
@ -687,7 +687,7 @@ export class DataProviderService implements OnModuleInit {
} else if (
dataProvider.getDataProviderInfo().isPremium &&
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user?.subscription?.type === SubscriptionType.Basic
subscriptionType === SubscriptionType.Basic
) {
// Skip symbols of Premium data providers for users without subscription
return false;

382
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

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

File diff suppressed because it is too large

382
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

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

File diff suppressed because it is too large

382
apps/client/src/locales/messages.ja.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.ko.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

382
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

381
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

586
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

18
libs/ui/src/lib/no-activities-info/no-activities-info.component.html

@ -2,12 +2,14 @@
<div class="d-flex justify-content-center mb-1">
<gf-logo size="large" [showLabel]="false" />
</div>
<a
class="align-items-center justify-content-center"
color="primary"
mat-button
[routerLink]="routerLinkPortfolioActivitiesCreate"
>
<span i18n>Time to add your first activity.</span>
</a>
<div class="d-flex justify-content-center">
<a
class="align-items-center d-inline-flex justify-content-center"
color="primary"
mat-button
[routerLink]="routerLinkPortfolioActivitiesCreate"
>
<span i18n>Add your first activity</span>
</a>
</div>
</div>

70
package-lock.json

@ -21,10 +21,10 @@
"@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2",
"@bull-board/api": "9.5.0",
"@bull-board/express": "9.5.0",
"@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15",
"@bull-board/api": "9.8.0",
"@bull-board/express": "9.8.0",
"@bull-board/nestjs": "9.8.0",
"@codewithdan/observable-store": "3.0.0",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",
"@ionic/angular": "8.8.12",
@ -90,7 +90,7 @@
"ngx-markdown": "22.0.0",
"ngx-skeleton-loader": "13.0.0",
"open-color": "1.9.1",
"papaparse": "5.5.3",
"papaparse": "5.7.0",
"passport": "0.7.0",
"passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2",
@ -106,7 +106,7 @@
"uuid": "14.0.2",
"yahoo-finance2": "4.0.2",
"zod": "4.5.4",
"zone.js": "0.16.2"
"zone.js": "0.16.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "22.1.4",
@ -6166,15 +6166,15 @@
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bull-board/api": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.5.0.tgz",
"integrity": "sha512-fmOyOy1lZiLZFyDJjI2osZrpTMO+JSgmGNhDf3MrmOf3oF084o6w0wpGefs0OqEfyUT1F5/1arE5AwmEUuxZ+A==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.8.0.tgz",
"integrity": "sha512-+qmJ19pBrOSZCPLbN93IijoPEpx5DZBWkwqUBk/FgQObvSyHB2OJvuwvvi/TaujTCbRH1eiYPc4USp9dTlNEug==",
"license": "MIT",
"dependencies": {
"redis-info": "^3.1.0"
},
"peerDependencies": {
"@bull-board/ui": "9.5.0",
"@bull-board/ui": "9.8.0",
"bull": "^4.16.5",
"bullmq": "^5.56.0 || ^6.0.0"
},
@ -6188,13 +6188,13 @@
}
},
"node_modules/@bull-board/express": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.5.0.tgz",
"integrity": "sha512-EKeXQzeb1zF9ra0hfYfPArU1JlWKl2vyL4Rb3gQW0XzU7SkEFgYm3727gDrQZdU8dvc5g/FLEqc0dr6UkDUHhg==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.8.0.tgz",
"integrity": "sha512-nYywx44PHHq7ybgQZySrDiM9B2Yc1NSywWm+zdjP8UyVGrvgooZ0UsT+F58ZF4wQ2QGqYRCTNb+YbPvBAWWjBg==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "9.5.0",
"@bull-board/ui": "9.5.0",
"@bull-board/api": "9.8.0",
"@bull-board/ui": "9.8.0",
"ejs": "^6.0.1",
"express": "^5.2.1"
}
@ -6212,26 +6212,26 @@
}
},
"node_modules/@bull-board/nestjs": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.5.0.tgz",
"integrity": "sha512-0P/Ru3cbOnf6GwnDGv4iPJ/NEjbcaicdpYMISpp6K7JZ24RVkCnmoUIKt8KbQdjZGqfH2EWmyIRC2WhiUE3T+g==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.8.0.tgz",
"integrity": "sha512-Ooht3SBI6/3kiqrQ8HXQf9cXNH7C7NQoCupA9F0FGGnozdMUfS4+c8IXCwpcTuOp6x1+o1ZAqY6bckPiYdngAg==",
"license": "MIT",
"peerDependencies": {
"@bull-board/api": "^9.5.0",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@bull-board/api": "^9.8.0",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0 || ^12.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.8.1"
}
},
"node_modules/@bull-board/ui": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.5.0.tgz",
"integrity": "sha512-qrq0e906M8Ps/mV3bQXmj7EXJWLaFs5mNz+oBoLR/7EShBi5YooROzQ/SEFFedg/L9h6QWDWJ9Ph0AH7NrG9uw==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.8.0.tgz",
"integrity": "sha512-WE6imvISwGDI/E1i7H1M4mQ/NI99sj9u/XIUT5QgbQlWAbys1N4/YdfPktwK6eyKynPMPHJqcAqwRDJDzlceOA==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "9.5.0"
"@bull-board/api": "9.8.0"
}
},
"node_modules/@cacheable/utils": {
@ -6246,9 +6246,9 @@
}
},
"node_modules/@codewithdan/observable-store": {
"version": "2.2.15",
"resolved": "https://registry.npmjs.org/@codewithdan/observable-store/-/observable-store-2.2.15.tgz",
"integrity": "sha512-LVCSMZzTCvoDo5n7YDmtIIEhTmvJ8O21k36Vwu/A4kumdXQ1YVs4sKoSK3vlINZPL4AYY2MRsBVtvre4QIETFw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@codewithdan/observable-store/-/observable-store-3.0.0.tgz",
"integrity": "sha512-OyHzU29/nszIs0+340uQYjA2Ftx0eGmU3TyLYK8/j1IW7pv1Qx/W68dWcfC26hV67SVschPpp5v/qKudx9Marg==",
"license": "MIT",
"peerDependencies": {
"rxjs": ">=6.4.0 <8"
@ -29413,9 +29413,9 @@
"license": "BlueOak-1.0.0"
},
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz",
"integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==",
"license": "MIT"
},
"node_modules/param-case": {
@ -37253,9 +37253,9 @@
}
},
"node_modules/zone.js": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.2.tgz",
"integrity": "sha512-Eky7p2Z1Ig3NnbfodSPoARCjKBSTFMnE/ACsP1L/XJEfY4SdOFce19BsUCWVwL6K5ABZFy5J3bjcMWffX+YM3Q==",
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.3.tgz",
"integrity": "sha512-ihXL9+vhYyEhXz4TDNpHeAOZN9FVrbog0Il64OIEI28UP/n5AaI6gsccRuOHBfx+206agzyoK527bYIO0Foy6A==",
"license": "MIT"
}
}

12
package.json

@ -65,10 +65,10 @@
"@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2",
"@bull-board/api": "9.5.0",
"@bull-board/express": "9.5.0",
"@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15",
"@bull-board/api": "9.8.0",
"@bull-board/express": "9.8.0",
"@bull-board/nestjs": "9.8.0",
"@codewithdan/observable-store": "3.0.0",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7",
"@ionic/angular": "8.8.12",
@ -134,7 +134,7 @@
"ngx-markdown": "22.0.0",
"ngx-skeleton-loader": "13.0.0",
"open-color": "1.9.1",
"papaparse": "5.5.3",
"papaparse": "5.7.0",
"passport": "0.7.0",
"passport-google-oauth20": "2.0.0",
"passport-headerapikey": "1.2.2",
@ -150,7 +150,7 @@
"uuid": "14.0.2",
"yahoo-finance2": "4.0.2",
"zod": "4.5.4",
"zone.js": "0.16.2"
"zone.js": "0.16.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "22.1.4",

Loading…
Cancel
Save