Browse Source

Merge branch 'main' into task/remove-deprecated-first-order-date-from-portfolio-performance-endpoint-response

pull/7483/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
cac3aacade
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 41
      CHANGELOG.md
  2. 43
      apps/api/src/app/activities/activities-filter.dto.ts
  3. 83
      apps/api/src/app/activities/activities.controller.ts
  4. 9
      apps/api/src/app/activities/activities.service.ts
  5. 27
      apps/api/src/app/activities/get-activities.dto.ts
  6. 7
      apps/api/src/app/admin/admin.service.ts
  7. 7
      apps/api/src/app/auth/api-key.strategy.ts
  8. 7
      apps/api/src/app/auth/jwt.strategy.ts
  9. 12
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  10. 21
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  11. 39
      apps/api/src/app/export/export.controller.ts
  12. 6
      apps/api/src/app/export/export.service.ts
  13. 14
      apps/api/src/app/export/get-export.dto.ts
  14. 4
      apps/api/src/app/health/health.service.ts
  15. 1
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  16. 14
      apps/api/src/app/portfolio/portfolio.service.ts
  17. 4
      apps/api/src/app/subscription/subscription.controller.ts
  18. 72
      apps/api/src/app/user/user.service.ts
  19. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  20. 7
      apps/api/src/services/data-provider/data-provider.service.ts
  21. 4
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  22. 70
      apps/api/src/services/property/property.service.ts
  23. 5
      apps/client/src/app/app.component.ts
  24. 1
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  25. 1
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  26. 1
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  27. 33
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  28. 296
      apps/client/src/locales/messages.ca.xlf
  29. 300
      apps/client/src/locales/messages.de.xlf
  30. 296
      apps/client/src/locales/messages.es.xlf
  31. 296
      apps/client/src/locales/messages.fr.xlf
  32. 296
      apps/client/src/locales/messages.it.xlf
  33. 296
      apps/client/src/locales/messages.ja.xlf
  34. 296
      apps/client/src/locales/messages.ko.xlf
  35. 296
      apps/client/src/locales/messages.nl.xlf
  36. 296
      apps/client/src/locales/messages.pl.xlf
  37. 296
      apps/client/src/locales/messages.pt.xlf
  38. 296
      apps/client/src/locales/messages.tr.xlf
  39. 296
      apps/client/src/locales/messages.uk.xlf
  40. 292
      apps/client/src/locales/messages.xlf
  41. 296
      apps/client/src/locales/messages.zh.xlf
  42. 11
      apps/client/src/styles.scss
  43. 6
      apps/client/src/styles/theme.scss
  44. 24
      libs/common/src/lib/calculation-helper.spec.ts
  45. 13
      libs/common/src/lib/calculation-helper.ts
  46. 19
      libs/common/src/lib/config.ts
  47. 5
      libs/common/src/lib/helper.ts
  48. 12
      libs/common/src/lib/types/date-range.type.ts
  49. 24
      libs/ui/src/lib/activities-table/activities-table.component.html
  50. 9
      libs/ui/src/lib/activities-table/activities-table.component.scss
  51. 25
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  52. 19
      libs/ui/src/lib/activities-table/activities-table.component.ts
  53. 9
      libs/ui/src/lib/assistant/assistant.component.ts
  54. 7
      libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html
  55. 34
      libs/ui/src/lib/services/data.service.ts
  56. 8
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  57. 596
      package-lock.json
  58. 10
      package.json

41
CHANGELOG.md

@ -10,6 +10,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Removed the deprecated `firstOrderDate` attribute from the `GET api/v2/portfolio/performance` endpoint response
- Improved the language localization for German (`de`)
- Upgraded `prisma` from version `7.8.0` to `7.9.1`
## 3.38.0 - 2026-07-31
### Added
- Added support for the date range filter in the export functionality
- Added support for the date range filter on the portfolio activities page
### Changed
- Improved the style of the tabs in the account detail dialog on mobile
- Improved the style of the tabs in the holding detail dialog on mobile
- Improved the style of the tabs in the asset profile dialog of the admin control panel on mobile
- Improved the style of the empty state in the _Fear & Greed Index_ component
- Added the activity count to the delete menu item of the activities table
- Added the activity count to the deletion confirmation dialog of the activities table
- Improved the style of the type filter in the activities table component (experimental)
- Improved the search functionality by trimming the query
- Improved the log output in the search functionality of the _Yahoo Finance_ service for unsupported queries
- Improved the performance of the property service by caching the properties in memory
- Improved the validation of the query parameters in the activities endpoints
- Improved the language localization for German (`de`)
### Fixed
- Fixed the calendar year date range in time zones with a negative _UTC_ offset
- Fixed the deletion of activities to respect the activity type filter on the activities page (experimental)
- Fixed the deletion of activities to respect the date range filter on the activities page
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Equity)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Fixed Income)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment: Base Currency)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Developed Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Asia-Pacific)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Europe)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Japan)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (North America)
## 3.37.0 - 2026-07-30

43
apps/api/src/app/activities/activities-filter.dto.ts

@ -0,0 +1,43 @@
import { DATE_RANGES } from '@ghostfolio/common/config';
import { DateRange } from '@ghostfolio/common/types';
import { Type as ActivityType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsEnum, IsOptional, IsString, Matches } from 'class-validator';
import { isString } from 'lodash';
// A named date range or a calendar year like '2024', '2023', '2022', etc.
const DATE_RANGE_PATTERN = new RegExp(`^(${DATE_RANGES.join('|')}|\\d{4})$`);
export class ActivitiesFilterDto {
@IsOptional()
@IsString()
accounts?: string;
@IsEnum(ActivityType, { each: true })
@IsOptional()
@Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
activityTypes?: ActivityType[];
@IsOptional()
@IsString()
assetClasses?: string;
@IsOptional()
@IsString()
dataSource?: string;
@IsOptional()
@Matches(DATE_RANGE_PATTERN)
range?: DateRange;
@IsOptional()
@IsString()
symbol?: string;
@IsOptional()
@IsString()
tags?: string;
}

83
apps/api/src/app/activities/activities.controller.ts

@ -18,7 +18,7 @@ import {
ActivityResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -29,7 +29,6 @@ import {
HttpException,
Inject,
Param,
ParseIntPipe,
Post,
Put,
Query,
@ -38,11 +37,13 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order, Prisma, Type as ActivityType } from '@prisma/client';
import { Order } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ActivitiesFilterDto } from './activities-filter.dto';
import { ActivitiesService } from './activities.service';
import { GetActivitiesDto } from './get-activities.dto';
@Controller('activities')
export class ActivitiesController {
@ -60,22 +61,38 @@ export class ActivitiesController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async deleteActivities(
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query() query: ActivitiesFilterDto
): Promise<number> {
if (impersonationId) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let endDate: Date;
let startDate: Date;
if (query.range) {
({ endDate, startDate } = getIntervalFromDateRange({
dateRange: query.range
}));
}
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: query.accounts,
filterByAssetClasses: query.assetClasses,
filterByDataSource: query.dataSource,
filterBySymbol: query.symbol,
filterByTags: query.tags
});
return this.activitiesService.deleteActivities({
endDate,
filters,
startDate,
types: query.activityTypes,
userId: this.request.user.id
});
}
@ -108,51 +125,41 @@ export class ActivitiesController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllActivities(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('activityTypes') filterByTypes?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange?: DateRange,
@Query('skip', new ParseIntPipe({ optional: true })) skip?: number,
@Query('sortColumn') sortColumn?: string,
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('take', new ParseIntPipe({ optional: true })) take?: number
@Query() query: GetActivitiesDto
): Promise<ActivitiesResponse> {
let endDate: Date;
let startDate: Date;
if (dateRange) {
({ endDate, startDate } = getIntervalFromDateRange({ dateRange }));
if (query.range) {
({ endDate, startDate } = getIntervalFromDateRange({
dateRange: query.range
}));
}
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: query.accounts,
filterByAssetClasses: query.assetClasses,
filterByDataSource: query.dataSource,
filterBySymbol: query.symbol,
filterByTags: query.tags
});
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const types = (filterByTypes?.split(',') as ActivityType[]) ?? [];
const userCurrency = this.request.user.settings.settings.baseCurrency;
const { activities, count } = await this.activitiesService.getActivities({
endDate,
filters,
skip,
sortColumn,
sortDirection,
startDate,
take,
types,
userCurrency,
includeDrafts: true,
skip: query.skip,
sortColumn: query.sortColumn,
sortDirection: query.sortDirection,
take: query.take,
types: query.activityTypes,
userId: impersonationUserId || this.request.user.id,
withExcludedAccountsAndActivities: true
});

9
apps/api/src/app/activities/activities.service.ts

@ -361,14 +361,23 @@ export class ActivitiesService {
}
public async deleteActivities({
endDate,
filters,
startDate,
types,
userId
}: {
endDate?: Date;
filters?: Filter[];
startDate?: Date;
types?: ActivityType[];
userId: string;
}): Promise<number> {
const { activities } = await this.getActivities({
endDate,
filters,
startDate,
types,
userId,
includeDrafts: true,
userCurrency: undefined,

27
apps/api/src/app/activities/get-activities.dto.ts

@ -0,0 +1,27 @@
import { Prisma } from '@prisma/client';
import { Type } from 'class-transformer';
import { IsIn, IsInt, IsOptional, Min } from 'class-validator';
import { ActivitiesFilterDto } from './activities-filter.dto';
export class GetActivitiesDto extends ActivitiesFilterDto {
@IsInt()
@IsOptional()
@Min(0)
@Type(() => Number)
skip?: number;
@IsIn(Object.values(Prisma.OrderScalarFieldEnum))
@IsOptional()
sortColumn?: keyof typeof Prisma.OrderScalarFieldEnum;
@IsIn(['asc', 'desc'] as Prisma.SortOrder[])
@IsOptional()
sortDirection?: Prisma.SortOrder;
@IsInt()
@IsOptional()
@Min(0)
@Type(() => Number)
take?: number;
}

7
apps/api/src/app/admin/admin.service.ts

@ -107,8 +107,11 @@ export class AdminService {
await this.marketDataService.deleteMany({ dataSource, symbol });
const currency = getCurrencyFromSymbol(symbol);
const customCurrencies =
await this.propertyService.getByKey<string[]>(PROPERTY_CURRENCIES);
const customCurrencies = await this.propertyService.getByKey<string[]>(
PROPERTY_CURRENCIES,
{ skipCache: true }
);
if (customCurrencies.includes(currency)) {
const updatedCustomCurrencies = customCurrencies.filter(

7
apps/api/src/app/auth/api-key.strategy.ts

@ -35,6 +35,13 @@ export class ApiKeyStrategy extends PassportStrategy(
);
}
if (await this.userService.isDailyRequestLimitExceeded({ user })) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.prismaService.analytics.upsert({
create: { user: { connect: { id: user.id } } },
update: {

7
apps/api/src/app/auth/jwt.strategy.ts

@ -42,6 +42,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
);
}
if (await this.userService.isDailyRequestLimitExceeded({ user })) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
const country =
countriesAndTimezones.getCountryForTimezone(timezone)?.id;

12
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -51,7 +51,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
@ -95,7 +95,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
@ -135,7 +135,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
@ -176,7 +176,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
@ -215,7 +215,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
@ -252,7 +252,7 @@ export class GhostfolioController {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),

21
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -17,7 +17,10 @@ import {
DERIVED_CURRENCIES
} from '@ghostfolio/common/config';
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
getAssetProfileIdentifier,
isValidSearchQuery
} from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderHistoricalResponse,
@ -326,8 +329,12 @@ export class GhostfolioService {
}
public async incrementDailyRequests({ userId }: { userId: string }) {
await this.prismaService.analytics.update({
data: {
await this.prismaService.analytics.upsert({
create: {
dataProviderGhostfolioDailyRequests: 1,
user: { connect: { id: userId } }
},
update: {
dataProviderGhostfolioDailyRequests: { increment: 1 }
},
where: { userId }
@ -340,7 +347,9 @@ export class GhostfolioService {
}: GetSearchParams): Promise<LookupResponse> {
const results: LookupResponse = { items: [] };
if (!query) {
query = query?.trim();
if (!isValidSearchQuery(query)) {
return results;
}
@ -348,10 +357,6 @@ export class GhostfolioService {
let lookupItems: LookupItem[] = [];
const promises: Promise<{ items: LookupItem[] }>[] = [];
if (query?.length < 2) {
return { items: lookupItems };
}
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService.search({

39
apps/api/src/app/export/export.controller.ts

@ -2,6 +2,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { ExportResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -15,9 +16,9 @@ import {
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Type as ActivityType } from '@prisma/client';
import { ExportService } from './export.service';
import { GetExportDto } from './get-export.dto';
@Controller('export')
export class ExportController {
@ -31,30 +32,30 @@ export class ExportController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async export(
@Query('accounts') filterByAccounts?: string,
@Query('activityIds') filterByActivityIds?: string,
@Query('activityTypes') filterByTypes?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise<ExportResponse> {
const activityIds = filterByActivityIds?.split(',') ?? [];
const activityTypes = (filterByTypes?.split(',') as ActivityType[]) ?? [];
public async export(@Query() query: GetExportDto): Promise<ExportResponse> {
let endDate: Date;
let startDate: Date;
if (query.range) {
({ endDate, startDate } = getIntervalFromDateRange({
dateRange: query.range
}));
}
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: query.accounts,
filterByAssetClasses: query.assetClasses,
filterByDataSource: query.dataSource,
filterBySymbol: query.symbol,
filterByTags: query.tags
});
return this.exportService.export({
activityIds,
activityTypes,
endDate,
filters,
startDate,
activityIds: query.activityIds,
activityTypes: query.activityTypes,
userId: this.request.user.id,
userSettings: this.request.user.settings.settings
});

6
apps/api/src/app/export/export.service.ts

@ -25,13 +25,17 @@ export class ExportService {
public async export({
activityIds,
activityTypes,
endDate,
filters,
startDate,
userId,
userSettings
}: {
activityIds?: string[];
activityTypes?: ActivityType[];
endDate?: Date;
filters?: Filter[];
startDate?: Date;
userId: string;
userSettings: UserSettings;
}): Promise<ExportResponse> {
@ -41,7 +45,9 @@ export class ExportService {
const platformsMap: { [platformId: string]: Platform } = {};
let { activities } = await this.activitiesService.getActivities({
endDate,
filters,
startDate,
userId,
includeDrafts: true,
sortColumn: 'date',

14
apps/api/src/app/export/get-export.dto.ts

@ -0,0 +1,14 @@
import { ActivitiesFilterDto } from '@ghostfolio/api/app/activities/activities-filter.dto';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsOptional, IsUUID } from 'class-validator';
import { isString } from 'lodash';
export class GetExportDto extends ActivitiesFilterDto {
@IsOptional()
@IsUUID(undefined, { each: true })
@Transform(({ value }: TransformFnParams) => {
return isString(value) ? value.split(',') : value;
})
activityIds?: string[];
}

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

@ -26,7 +26,9 @@ export class HealthService {
public async isDatabaseHealthy() {
try {
await this.propertyService.getByKey(PROPERTY_CURRENCIES);
await this.propertyService.getByKey(PROPERTY_CURRENCIES, {
skipCache: true
});
return true;
} catch {

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

@ -92,6 +92,7 @@ describe('PortfolioService', () => {
null,
null,
null,
null,
null
);

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

@ -1124,6 +1124,8 @@ export class PortfolioService {
withSummary: true
});
const hasOpenHoldings = Object.keys(holdings).length > 0;
const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency);
@ -1183,8 +1185,7 @@ export class PortfolioService {
id: 'rule.currencyClusterRisk.category',
languageCode: userSettings.language
}),
rules:
summary.activityCount > 0
rules: hasOpenHoldings
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
@ -1210,8 +1211,7 @@ export class PortfolioService {
id: 'rule.assetClassClusterRisk.category',
languageCode: userSettings.language
}),
rules:
summary.activityCount > 0
rules: hasOpenHoldings
? await this.rulesService.evaluate(
[
new AssetClassClusterRiskEquity(
@ -1264,8 +1264,7 @@ export class PortfolioService {
id: 'rule.economicMarketClusterRisk.category',
languageCode: userSettings.language
}),
rules:
summary.activityCount > 0
rules: hasOpenHoldings
? await this.rulesService.evaluate(
[
new EconomicMarketClusterRiskDevelopedMarkets(
@ -1293,8 +1292,7 @@ export class PortfolioService {
id: 'rule.regionalMarketClusterRisk.category',
languageCode: userSettings.language
}),
rules:
summary.activityCount > 0
rules: hasOpenHoldings
? await this.rulesService.evaluate(
[
new RegionalMarketClusterRiskAsiaPacific(

4
apps/api/src/app/subscription/subscription.controller.ts

@ -54,7 +54,9 @@ export class SubscriptionController {
}
let coupons =
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS)) ?? [];
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS, {
skipCache: true
})) ?? [];
const coupon = coupons.find((currentCoupon) => {
return currentCoupon.code === couponCode;

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

@ -31,9 +31,12 @@ import {
DEFAULT_LOCALE,
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_MAX_DAILY_REQUESTS,
PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
TAG_ID_EXCLUDE_FROM_ANALYSIS
TAG_ID_EXCLUDE_FROM_ANALYSIS,
THROTTLE_DAILY_KEY,
THROTTLE_DAILY_TTL
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
@ -50,15 +53,18 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler';
import { Prisma, Role, Settings, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns';
import { without } from 'lodash';
import { isNil, without } from 'lodash';
import { createHmac } from 'node:crypto';
@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);
public constructor(
private readonly activitiesService: ActivitiesService,
private readonly configurationService: ConfigurationService,
@ -67,7 +73,9 @@ export class UserService {
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService,
private readonly tagService: TagService
private readonly tagService: TagService,
@InjectThrottlerStorage()
private readonly throttlerStorage: ThrottlerStorage
) {}
public async count(args?: Prisma.UserCountArgs) {
@ -228,6 +236,38 @@ export class UserService {
return usersWithAdminRole.length > 0;
}
public async isDailyRequestLimitExceeded({
user
}: {
user: UserWithSettings;
}) {
if (user.subscription?.type === SubscriptionType.Premium) {
return false;
}
const maxDailyRequests = await this.getMaxDailyRequests();
if (maxDailyRequests === undefined) {
return false;
}
try {
const { isBlocked } = await this.throttlerStorage.increment(
`${THROTTLE_DAILY_KEY}-${user.id}`,
THROTTLE_DAILY_TTL,
maxDailyRequests,
THROTTLE_DAILY_TTL,
THROTTLE_DAILY_KEY
);
return isBlocked;
} catch (error) {
this.logger.error(error);
return false;
}
}
public async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> {
@ -286,7 +326,7 @@ export class UserService {
updatedAt,
activityCount: analytics?.activityCount,
dataProviderGhostfolioDailyRequests:
analytics?.dataProviderGhostfolioDailyRequests
analytics?.dataProviderGhostfolioDailyRequests ?? 0
};
if (user.settings) {
@ -782,4 +822,26 @@ export class UserService {
return settings;
}
private async getMaxDailyRequests() {
const value = await this.propertyService.getByKey<string>(
PROPERTY_MAX_DAILY_REQUESTS
);
if (isNil(value) || value === '') {
return undefined;
}
const maxDailyRequests = Number(value);
if (!Number.isInteger(maxDailyRequests) || maxDailyRequests < 0) {
this.logger.warn(
`The property ${PROPERTY_MAX_DAILY_REQUESTS} is not a non-negative integer ("${value}"), the daily request limit is not applied`
);
return undefined;
}
return maxDailyRequests;
}
}

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -159,7 +159,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS
PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id });
@ -196,7 +197,8 @@ export class BenchmarkService {
let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS
PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => {

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

@ -20,7 +20,8 @@ import {
getCurrencyFromSymbol,
getStartOfUtcDate,
isCurrency,
isDerivedCurrency
isDerivedCurrency,
isValidSearchQuery
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
@ -838,7 +839,9 @@ export class DataProviderService implements OnModuleInit {
let lookupItems: LookupItem[] = [];
const promises: Promise<LookupResponse>[] = [];
if (query?.length < 2) {
query = query?.trim();
if (!isValidSearchQuery(query)) {
return { items: lookupItems };
}

4
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -330,8 +330,12 @@ export class YahooFinanceService implements DataProviderInterface {
});
}
} catch (error) {
if (error?.name === 'BadRequestError') {
this.logger.warn(`Could not search for "${query}": ${error.message}`);
} else {
this.logger.error(error);
}
}
return { items };
}

70
apps/api/src/services/property/property.service.ts

@ -6,27 +6,39 @@ import {
import { PropertyKey } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns';
import ms from 'ms';
import { PropertyValue } from './interfaces/interfaces';
@Injectable()
export class PropertyService {
private static readonly CACHE_TTL = ms('1 minute');
private cachedProperties: Promise<Property[]>;
private cachedPropertiesExpiresAt: Date;
public constructor(private readonly prismaService: PrismaService) {}
public async delete({ key }: { key: PropertyKey }) {
return this.prismaService.property.delete({
const property = await this.prismaService.property.delete({
where: { key }
});
this.invalidateCache();
return property;
}
public async get() {
public async get({ skipCache = false } = {}) {
const response: {
[key: string]: PropertyValue;
} = {
[PROPERTY_CURRENCIES]: []
};
const properties = await this.prismaService.property.findMany();
const properties = await this.getProperties({ skipCache });
for (const property of properties) {
let value = property.value;
@ -41,8 +53,11 @@ export class PropertyService {
return response;
}
public async getByKey<TValue extends PropertyValue>(aKey: PropertyKey) {
const properties = await this.get();
public async getByKey<TValue extends PropertyValue>(
aKey: PropertyKey,
{ skipCache = false } = {}
) {
const properties = await this.get({ skipCache });
return properties[aKey] as TValue;
}
@ -53,10 +68,53 @@ export class PropertyService {
}
public async put({ key, value }: { key: PropertyKey; value: string }) {
return this.prismaService.property.upsert({
const property = await this.prismaService.property.upsert({
create: { key, value },
update: { value },
where: { key }
});
this.invalidateCache();
return property;
}
/**
* Returns the properties from the in-memory cache, falling back to the
* database. Callers which write back a modified property must set
* skipCache to avoid basing the write on a stale read.
*/
private async getProperties({ skipCache = false } = {}) {
if (skipCache) {
return this.prismaService.property.findMany();
}
if (
this.cachedProperties &&
isBefore(new Date(), this.cachedPropertiesExpiresAt)
) {
return this.cachedProperties;
}
const properties = this.prismaService.property.findMany().catch((error) => {
if (this.cachedProperties === properties) {
this.invalidateCache();
}
throw error;
});
this.cachedProperties = properties;
this.cachedPropertiesExpiresAt = addMilliseconds(
new Date(),
PropertyService.CACHE_TTL
);
return this.cachedProperties;
}
private invalidateCache() {
this.cachedProperties = undefined;
this.cachedPropertiesExpiresAt = undefined;
}
}

5
apps/client/src/app/app.component.ts

@ -135,7 +135,10 @@ export class GfAppComponent implements OnInit {
this.currentSubRoute ===
internalRoutes.home.subRoutes?.holdings.path) ||
(this.currentRoute === internalRoutes.portfolio.path &&
!this.currentSubRoute)) &&
!this.currentSubRoute) ||
(this.currentRoute === internalRoutes.portfolio.path &&
this.currentSubRoute ===
internalRoutes.portfolio.subRoutes?.activities.path)) &&
this.user?.settings?.viewMode !== 'ZEN'
) {
this.hasPermissionToChangeDateRange = true;

1
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -33,6 +33,7 @@
<mat-tab-group
animationDuration="0ms"
class="mb-4"
[disablePagination]="true"
[mat-stretch-tabs]="false"
>
<mat-tab>

1
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -105,6 +105,7 @@
<mat-tab-group
animationDuration="0ms"
[disablePagination]="true"
[dynamicHeight]="true"
[mat-stretch-tabs]="false"
>

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

@ -39,6 +39,7 @@
<mat-tab-group
animationDuration="0ms"
class="mb-4"
[disablePagination]="true"
[mat-stretch-tabs]="false"
>
<mat-tab>

33
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -10,7 +10,6 @@ import {
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { DateRange } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfFabComponent } from '@ghostfolio/ui/fab';
import { DataService } from '@ghostfolio/ui/services';
@ -92,8 +91,14 @@ export class GfActivitiesPageComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
const previousDateRange = this.getDateRange();
this.updateUser(state.user);
if (previousDateRange !== this.getDateRange()) {
this.pageIndex = 0;
}
this.fetchActivities();
this.changeDetectorRef.markForCheck();
@ -120,7 +125,11 @@ export class GfActivitiesPageComponent implements OnInit {
protected onDeleteActivities() {
this.dataService
.deleteActivities({
filters: this.userService.getFilters()
activityTypes: this.activityTypesFilter.length
? this.activityTypesFilter
: undefined,
filters: this.userService.getFilters(),
range: this.getDateRange()
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
@ -159,7 +168,8 @@ export class GfActivitiesPageComponent implements OnInit {
activityTypes: this.activityTypesFilter.length
? this.activityTypesFilter
: undefined,
filters: this.userService.getFilters()
filters: this.userService.getFilters(),
range: this.getDateRange()
};
}
@ -277,16 +287,13 @@ export class GfActivitiesPageComponent implements OnInit {
this.dataSource = undefined;
this.totalItems = undefined;
const dateRange = this.user?.settings?.dateRange;
const range = this.isCalendarYear(dateRange) ? dateRange : undefined;
this.dataService
.fetchActivities({
range,
activityTypes: this.activityTypesFilter.length
? this.activityTypesFilter
: undefined,
filters: this.userService.getFilters(),
range: this.getDateRange(),
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
@ -311,12 +318,16 @@ export class GfActivitiesPageComponent implements OnInit {
});
}
private isCalendarYear(dateRange?: DateRange) {
if (!dateRange) {
return false;
private getDateRange() {
const dateRange = this.user?.settings?.dateRange;
// Omit the date ranges which do not apply to activities: '1d' spans today
// only, while 'max' would exclude drafts dated in the future
if (!dateRange || ['1d', 'max'].includes(dateRange)) {
return undefined;
}
return /^\d{4}$/.test(dateRange);
return dateRange;
}
private updateUser(aUser: User) {

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

11
apps/client/src/styles.scss

@ -411,6 +411,17 @@ ngx-skeleton-loader {
.mdc-dialog__content {
--mat-dialog-supporting-text-color: rgba(var(--dark-primary-text));
}
@media (max-width: 575.98px) {
// Tabs fill the available width on mobile
.mat-mdc-tab-group {
.mat-mdc-tab {
flex-grow: 1;
min-width: unset;
padding: 0 0.5rem;
}
}
}
}
.mat-mdc-fab,

6
apps/client/src/styles/theme.scss

@ -364,6 +364,12 @@ $gf-typography: (
)
);
@include mat.pseudo-checkbox-overrides(
(
full-selected-icon-color: var(--gf-theme-primary-500)
)
);
@include mat.slide-toggle-overrides(
(
selected-focus-handle-color: var(--gf-theme-primary-500),

24
libs/common/src/lib/calculation-helper.spec.ts

@ -1,8 +1,30 @@
import { Big } from 'big.js';
import { format } from 'date-fns';
import { getAnnualizedPerformancePercent } from './calculation-helper';
import {
getAnnualizedPerformancePercent,
getIntervalFromDateRange
} from './calculation-helper';
import { DATE_FORMAT } from './helper';
describe('CalculationHelper', () => {
describe('interval from date range', () => {
it('Get interval of a calendar year', async () => {
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: '2024'
});
// The boundaries are expressed in the local time zone, therefore the
// calendar days must hold independently of the time zone the tests run in
expect(format(startDate, DATE_FORMAT)).toEqual('2023-12-31');
expect(format(endDate, DATE_FORMAT)).toEqual('2024-12-31');
// The start date is exclusive, hence the first instant of the year is
// part of the interval
expect(startDate.getTime()).toEqual(new Date(2024, 0, 1).getTime() - 1);
});
});
describe('annualized performance percentage', () => {
it('Get annualized performance', async () => {
expect(

13
libs/common/src/lib/calculation-helper.ts

@ -7,6 +7,7 @@ import {
startOfWeek,
startOfYear,
subDays,
subMilliseconds,
subYears
} from 'date-fns';
import { isFinite, isNumber } from 'lodash';
@ -75,10 +76,16 @@ export function getIntervalFromDateRange(params: {
break;
case 'max':
break;
default:
default: {
// '2024', '2023', '2022', etc.
endDate = endOfYear(new Date(dateRange));
startDate = max([startDate, new Date(dateRange)]);
const yearStartDate = new Date(Number(dateRange), 0, 1);
// Derive the boundaries of the calendar year in the local time zone, as
// the consumers apply local time zone semantics. As the start date is
// exclusive, the last millisecond of the preceding year is used.
endDate = endOfYear(yearStartDate);
startDate = max([startDate, subMilliseconds(yearStartDate, 1)]);
}
}
return { endDate, startDate };

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

@ -67,6 +67,20 @@ export const DATA_GATHERING_QUEUE_PRIORITY_MEDIUM = Math.round(
DATA_GATHERING_QUEUE_PRIORITY_LOW / 2
);
/**
* The named date ranges, complemented by the calendar years like '2024',
* '2023', '2022', etc.
*/
export const DATE_RANGES = [
'1d',
'1y',
'5y',
'max',
'mtd',
'wtd',
'ytd'
] as const;
export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE =
'PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE';
export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH = 1;
@ -253,6 +267,7 @@ export const PROPERTY_DEMO_USER_ID = 'DEMO_USER_ID';
export const PROPERTY_IS_DATA_GATHERING_ENABLED = 'IS_DATA_GATHERING_ENABLED';
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
export const PROPERTY_IS_USER_SIGNUP_ENABLED = 'IS_USER_SIGNUP_ENABLED';
export const PROPERTY_MAX_DAILY_REQUESTS = 'MAX_DAILY_REQUESTS';
export const PROPERTY_OPENROUTER_MODEL = 'OPENROUTER_MODEL';
export const PROPERTY_OPENROUTER_MODEL_WEB_FETCH = 'OPENROUTER_MODEL_WEB_FETCH';
export const PROPERTY_PROXY_ROUTES = 'PROXY_ROUTES';
@ -287,6 +302,8 @@ export const REPLACE_NAME_PARTS = [
'Xtrackers (IE) Plc -'
];
export const SEARCH_QUERY_MINIMUM_LENGTH = 2;
export const SECTORS = [
'Basic Materials',
'Communication Services',
@ -326,6 +343,8 @@ export const TAG_ID_EXCLUDE_FROM_ANALYSIS =
'f2e868af-8333-459f-b161-cbc6544c24bd';
export const TAG_ID_DEMO = 'efa08cb3-9b9d-4974-ac68-db13a19c4874';
export const THROTTLE_DAILY_KEY = 'daily';
export const THROTTLE_DAILY_TTL = ms('1 day');
export const THROTTLE_DEFAULT_LIMIT = 10;
export const THROTTLE_DEFAULT_TTL = ms('1 minute');
export const THROTTLE_SIGNUP_LIMIT = 5;

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

@ -41,6 +41,7 @@ import {
DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks,
SEARCH_QUERY_MINIMUM_LENGTH,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from './config';
import {
@ -509,6 +510,10 @@ export function isRootCurrency(aCurrency: string) {
});
}
export function isValidSearchQuery(aQuery: string) {
return aQuery?.trim().length >= SEARCH_QUERY_MINIMUM_LENGTH;
}
export function parseDate(date: string): Date | undefined {
if (!date) {
return undefined;

12
libs/common/src/lib/types/date-range.type.ts

@ -1,9 +1,3 @@
export type DateRange =
| '1d'
| '1y'
| '5y'
| 'max'
| 'mtd'
| 'wtd'
| 'ytd'
| string; // '2024', '2023', '2022', etc.
import type { DATE_RANGES } from '../config';
export type DateRange = (typeof DATE_RANGES)[number] | string; // '2024', '2023', '2022', etc.

24
libs/ui/src/lib/activities-table/activities-table.component.html

@ -1,4 +1,6 @@
<div class="d-flex justify-content-end justify-content-lg-between">
<div
class="align-items-center d-flex justify-content-end justify-content-lg-between"
>
<div class="d-none d-lg-flex">
@if (hasPermissionToFilterByType) {
<mat-form-field appearance="outline" class="without-hint">
@ -56,12 +58,19 @@
<button
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource()?.data.length === 0"
[disabled]="!canExportActivities()"
(click)="onExport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline" />
@if (canExportActivities()) {
<span i18n
>Export {{ totalItems > 1 ? totalItems : '' }}
{totalItems, plural, =1 {Activity} other {Activities}}</span
>
} @else {
<span i18n>Export Activities</span>
}
</span>
</button>
}
@ -82,14 +91,19 @@
<button
class="align-items-center d-flex"
mat-menu-item
[disabled]="
dataSource()?.data.length === 0 || !hasPermissionToDeleteActivity
"
[disabled]="!canDeleteActivities()"
(click)="onDeleteActivities()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
@if (canDeleteActivities()) {
<span i18n
>Delete {{ totalItems > 1 ? totalItems : '' }}
{totalItems, plural, =1 {Activity} other {Activities}}</span
>
} @else {
<span i18n>Delete Activities</span>
}
</span>
</button>
</mat-menu>

9
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -1,3 +1,12 @@
@use '@angular/material' as mat;
:host {
display: block;
@include mat.form-field-overrides(
(
container-height: 2rem,
container-vertical-padding: 0.33rem
)
);
}

25
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -494,3 +494,28 @@ export const Actions: Story = {
totalItems: activities.length
}
};
export const Toolbar: Story = {
args: {
dataSource,
baseCurrency: 'USD',
deviceType: 'desktop',
hasActivities: true,
hasPermissionToCreateActivity: true,
hasPermissionToDeleteActivity: true,
hasPermissionToExportActivities: true,
hasPermissionToFilterByType: true,
hasPermissionToOpenDetails: false,
locale: 'en-US',
pageIndex: 0,
pageSize: 10,
showAccountColumn: true,
showActions: false,
showCheckbox: false,
showNameColumn: true,
sortColumn: 'date',
sortDirection: 'desc',
sortDisabled: false,
totalItems: activities.length
}
};

19
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -282,6 +282,20 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
);
}
public canDeleteActivities() {
return (
(this.dataSource()?.data.length ?? 0) > 0 &&
this.hasPermissionToDeleteActivity
);
}
public canExportActivities() {
return (
(this.dataSource()?.data.length ?? 0) > 0 &&
this.hasPermissionToExportActivities
);
}
public isExcludedFromAnalysis(activity: Activity) {
return (
(activity.account && isAccountExcluded(activity.account)) ??
@ -314,7 +328,10 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
this.activitiesDeleted.emit();
},
confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to delete these activities?`
title:
this.totalItems === 1
? $localize`Do you really want to delete this activity?`
: $localize`Do you really want to delete these ${this.totalItems}:count: activities?`
});
}

9
libs/ui/src/lib/assistant/assistant.component.ts

@ -202,6 +202,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.searchFormControl.valueChanges
.pipe(
map((searchTerm) => {
return searchTerm?.trim();
}),
debounceTime(300),
distinctUntilChanged(),
tap(() => {
this.isLoading = {
accounts: true,
assetProfiles: true,
@ -216,11 +221,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
};
this.changeDetectorRef.markForCheck();
return searchTerm?.trim();
}),
debounceTime(300),
distinctUntilChanged(),
switchMap((searchTerm) => {
const results = {
accounts: [],

7
libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html

@ -12,10 +12,11 @@
<div>
<div class="h4 mb-0">
<span class="mr-2">{{ fearAndGreedIndexText() }}</span>
@let value = fearAndGreedIndex() | number: '1.0-0';
<small class="text-muted"
><strong>{{
(fearAndGreedIndex() | number: '1.0-0') ?? placeholder
}}</strong
><span [class.font-weight-bold]="value">{{
value ?? placeholder
}}</span
>/100</small
>
</div>

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

@ -329,8 +329,24 @@ export class DataService {
return this.http.delete<AccountBalance>(`/api/v1/account-balance/${aId}`);
}
public deleteActivities({ filters }: { filters?: Filter[] }) {
const params = this.buildFiltersAsQueryParams({ filters });
public deleteActivities({
activityTypes,
filters,
range
}: {
activityTypes?: string[];
filters?: Filter[];
range?: DateRange;
}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (activityTypes?.length) {
params = params.append('activityTypes', activityTypes.join(','));
}
if (range) {
params = params.append('range', range);
}
return this.http.delete<number>('/api/v1/activities', { params });
}
@ -459,11 +475,13 @@ export class DataService {
public fetchExport({
activityIds,
activityTypes,
filters
filters,
range
}: {
activityIds?: string[];
activityTypes?: string[];
filters?: Filter[];
range?: DateRange;
} = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
@ -475,6 +493,10 @@ export class DataService {
params = params.append('activityTypes', activityTypes.join(','));
}
if (range) {
params = params.append('range', range);
}
return this.http.get<ExportResponse>('/api/v1/export', {
params
});
@ -509,8 +531,7 @@ export class DataService {
public fetchInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = window.localStorage.getItem('utm_source') as
| 'ios'
| 'trusted-web-activity';
'ios' | 'trusted-web-activity';
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
@ -949,8 +970,7 @@ export class DataService {
public updateInfo() {
this.http.get<InfoItem>('/api/v1/info').subscribe((info) => {
const utmSource = window.localStorage.getItem('utm_source') as
| 'ios'
| 'trusted-web-activity';
'ios' | 'trusted-web-activity';
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,

8
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -41,6 +41,7 @@ import {
debounceTime,
distinctUntilChanged,
filter,
map,
switchMap
} from 'rxjs/operators';
@ -128,6 +129,9 @@ export class GfSymbolAutocompleteComponent
this.control.valueChanges
.pipe(
map((query) => {
return isString(query) ? query.trim() : query;
}),
filter((query) => {
if (query?.length === 0) {
this.showDefaultOptions();
@ -137,13 +141,13 @@ export class GfSymbolAutocompleteComponent
return isString(query);
}),
debounceTime(400),
distinctUntilChanged(),
tap(() => {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
}),
debounceTime(400),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef),
switchMap((query: string) => {
return this.dataService.fetchSymbols({

596
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.37.0",
"version": "3.38.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.37.0",
"version": "3.38.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -43,8 +43,8 @@
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",
"@prisma/adapter-pg": "7.9.1",
"@prisma/client": "7.9.1",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1",
"ai": "7.0.37",
@ -127,7 +127,7 @@
"@nx/storybook": "23.0.2",
"@nx/web": "23.0.2",
"@nx/workspace": "23.0.2",
"@prisma/config": "7.8.0",
"@prisma/config": "7.9.1",
"@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10",
"@storybook/addon-themes": "10.1.10",
@ -157,7 +157,7 @@
"nx": "23.0.2",
"prettier": "3.9.6",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.8.0",
"prisma": "7.9.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.4.0",
@ -3908,33 +3908,33 @@
}
},
"node_modules/@electric-sql/pglite": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
"integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz",
"integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@electric-sql/pglite-socket": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz",
"integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==",
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz",
"integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"pglite-server": "dist/scripts/server.js"
},
"peerDependencies": {
"@electric-sql/pglite": "0.4.1"
"@electric-sql/pglite": "0.4.3"
}
},
"node_modules/@electric-sql/pglite-tools": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz",
"integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==",
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz",
"integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==",
"devOptional": true,
"license": "Apache-2.0",
"peerDependencies": {
"@electric-sql/pglite": "0.4.1"
"@electric-sql/pglite": "0.4.3"
}
},
"node_modules/@emnapi/core": {
@ -10041,24 +10041,24 @@
}
},
"node_modules/@prisma/adapter-pg": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz",
"integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz",
"integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/driver-adapter-utils": "7.8.0",
"@prisma/driver-adapter-utils": "7.9.1",
"@types/pg": "^8.16.0",
"pg": "^8.16.3",
"postgres-array": "3.0.4"
}
},
"node_modules/@prisma/client": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz",
"integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.9.1.tgz",
"integrity": "sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/client-runtime-utils": "7.8.0"
"@prisma/client-runtime-utils": "7.9.1"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24.0"
@ -10077,15 +10077,15 @@
}
},
"node_modules/@prisma/client-runtime-utils": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz",
"integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.9.1.tgz",
"integrity": "sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==",
"license": "Apache-2.0"
},
"node_modules/@prisma/config": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz",
"integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.1.tgz",
"integrity": "sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
@ -10096,97 +10096,95 @@
}
},
"node_modules/@prisma/debug": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz",
"integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz",
"integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==",
"license": "Apache-2.0"
},
"node_modules/@prisma/dev": {
"version": "0.24.3",
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz",
"integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==",
"version": "0.24.17",
"resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz",
"integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"@electric-sql/pglite": "0.4.1",
"@electric-sql/pglite-socket": "0.1.1",
"@electric-sql/pglite-tools": "0.3.1",
"@hono/node-server": "1.19.11",
"@electric-sql/pglite": "0.4.3",
"@electric-sql/pglite-socket": "0.1.3",
"@electric-sql/pglite-tools": "0.3.3",
"@prisma/get-platform": "7.2.0",
"@prisma/query-plan-executor": "7.2.0",
"@prisma/streams-local": "0.1.2",
"@prisma/streams-local": "0.1.11",
"find-my-way": "9.7.0",
"foreground-child": "3.3.1",
"get-port-please": "3.2.0",
"hono": "^4.12.8",
"http-status-codes": "2.3.0",
"pathe": "2.0.3",
"proper-lockfile": "4.1.2",
"remeda": "2.33.4",
"std-env": "3.10.0",
"valibot": "1.2.0",
"valibot": "1.4.2",
"zeptomatch": "2.1.0"
}
},
"node_modules/@prisma/driver-adapter-utils": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz",
"integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz",
"integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.8.0"
"@prisma/debug": "7.9.1"
}
},
"node_modules/@prisma/engines": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz",
"integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz",
"integrity": "sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.8.0",
"@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a",
"@prisma/fetch-engine": "7.8.0",
"@prisma/get-platform": "7.8.0"
"@prisma/debug": "7.9.1",
"@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad",
"@prisma/fetch-engine": "7.9.1",
"@prisma/get-platform": "7.9.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz",
"integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==",
"version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz",
"integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines/node_modules/@prisma/get-platform": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz",
"integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.8.0"
"@prisma/debug": "7.9.1"
}
},
"node_modules/@prisma/fetch-engine": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz",
"integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.1.tgz",
"integrity": "sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.8.0",
"@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a",
"@prisma/get-platform": "7.8.0"
"@prisma/debug": "7.9.1",
"@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad",
"@prisma/get-platform": "7.9.1"
}
},
"node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz",
"integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz",
"integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "7.8.0"
"@prisma/debug": "7.9.1"
}
},
"node_modules/@prisma/get-platform": {
@ -10214,9 +10212,9 @@
"license": "Apache-2.0"
},
"node_modules/@prisma/streams-local": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz",
"integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==",
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz",
"integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
@ -10226,7 +10224,7 @@
"proper-lockfile": "^4.1.2"
},
"engines": {
"bun": ">=1.3.6",
"bun": ">=1.2.0",
"node": ">=22.0.0"
}
},
@ -10244,14 +10242,23 @@
}
},
"node_modules/@prisma/studio-core": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz",
"integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==",
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz",
"integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@radix-ui/react-toggle": "1.1.10",
"chart.js": "4.5.1"
"@visx/curve": "4.0.1-alpha.0",
"@visx/event": "4.0.1-alpha.0",
"@visx/grid": "4.0.1-alpha.0",
"@visx/group": "4.0.1-alpha.0",
"@visx/responsive": "4.0.1-alpha.0",
"@visx/scale": "4.0.1-alpha.0",
"@visx/shape": "4.0.1-alpha.0",
"d3-array": "3.2.4",
"d3-shape": "3.2.0",
"elkjs": "0.11.1"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24.0",
@ -12501,9 +12508,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/geojson": "*"
}
@ -12531,9 +12537,8 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT",
"optional": true,
"peer": true
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/d3-polygon": {
"version": "3.0.2",
@ -12756,9 +12761,8 @@
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT",
"optional": true,
"peer": true
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/google-spreadsheet": {
"version": "3.1.5",
@ -12902,7 +12906,7 @@
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/luxon": {
@ -13056,7 +13060,6 @@
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@ -14168,6 +14171,265 @@
"node": ">= 20"
}
},
"node_modules/@visx/curve": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz",
"integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@visx/vendor": "4.0.0-alpha.0"
}
},
"node_modules/@visx/event": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz",
"integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"@visx/point": "4.0.1-alpha.0"
}
},
"node_modules/@visx/grid": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz",
"integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"@visx/curve": "4.0.1-alpha.0",
"@visx/group": "4.0.1-alpha.0",
"@visx/point": "4.0.1-alpha.0",
"@visx/scale": "4.0.1-alpha.0",
"@visx/shape": "4.0.1-alpha.0",
"classnames": "^2.3.1"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
}
},
"node_modules/@visx/group": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz",
"integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"classnames": "^2.3.1"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
}
},
"node_modules/@visx/point": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz",
"integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/responsive": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz",
"integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "^4.17.13",
"@types/react": "*",
"lodash": "^4.17.21"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
}
},
"node_modules/@visx/scale": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz",
"integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@visx/vendor": "4.0.0-alpha.0"
}
},
"node_modules/@visx/shape": {
"version": "4.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz",
"integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "^4.17.13",
"@types/react": "*",
"@visx/curve": "4.0.1-alpha.0",
"@visx/group": "4.0.1-alpha.0",
"@visx/scale": "4.0.1-alpha.0",
"@visx/vendor": "4.0.0-alpha.0",
"classnames": "^2.3.1",
"lodash": "^4.17.21"
},
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0"
}
},
"node_modules/@visx/vendor": {
"version": "4.0.0-alpha.0",
"resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz",
"integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==",
"devOptional": true,
"license": "MIT and ISC",
"dependencies": {
"@types/d3-array": "3.0.3",
"@types/d3-color": "3.1.0",
"@types/d3-delaunay": "6.0.1",
"@types/d3-format": "3.0.1",
"@types/d3-geo": "3.1.0",
"@types/d3-interpolate": "3.0.1",
"@types/d3-path": "3.1.1",
"@types/d3-scale": "4.0.2",
"@types/d3-shape": "3.1.7",
"@types/d3-time": "3.0.0",
"@types/d3-time-format": "2.1.0",
"d3-array": "3.2.1",
"d3-color": "3.1.0",
"d3-delaunay": "6.0.2",
"d3-format": "3.1.0",
"d3-geo": "3.1.0",
"d3-interpolate": "3.0.1",
"d3-path": "3.1.0",
"d3-scale": "4.0.2",
"d3-shape": "3.2.0",
"d3-time": "3.1.0",
"d3-time-format": "4.1.0",
"internmap": "2.0.3"
}
},
"node_modules/@visx/vendor/node_modules/@types/d3-array": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
"integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/@types/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/@types/d3-delaunay": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
"integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/@types/d3-format": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/@types/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@visx/vendor/node_modules/@types/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@visx/vendor/node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@visx/vendor/node_modules/@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/@types/d3-time-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz",
"integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@visx/vendor/node_modules/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@visx/vendor/node_modules/d3-delaunay": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz",
"integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@visx/vendor/node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"devOptional": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/@visx/vendor/node_modules/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
"devOptional": true,
"license": "ISC",
"dependencies": {
"d3-array": "2.5.0 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz",
@ -15462,9 +15724,9 @@
}
},
"node_modules/better-result": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.2.tgz",
"integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz",
"integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==",
"devOptional": true,
"license": "MIT"
},
@ -16392,6 +16654,13 @@
"validator": "^13.15.22"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"devOptional": true,
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@ -18049,8 +18318,7 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/cytoscape": {
"version": "3.33.2",
@ -18157,9 +18425,8 @@
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"internmap": "1 - 2"
},
@ -18214,9 +18481,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@ -18372,9 +18638,8 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@ -18408,9 +18673,8 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"d3-color": "1 - 3"
},
@ -18422,9 +18686,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@ -18516,9 +18779,8 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
@ -18560,9 +18822,8 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"d3-path": "^3.1.0"
},
@ -18574,9 +18835,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"d3-array": "2 - 3"
},
@ -18588,9 +18848,8 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"d3-time": "1 - 3"
},
@ -18943,9 +19202,8 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
"integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"robust-predicates": "^3.0.2"
}
@ -19286,6 +19544,13 @@
"integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
"license": "ISC"
},
"node_modules/elkjs": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
"integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
"devOptional": true,
"license": "EPL-2.0"
},
"node_modules/emittery": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
@ -20460,6 +20725,13 @@
],
"license": "MIT"
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
"integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -20510,6 +20782,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-querystring": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
"integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"fast-decode-uri-component": "^1.0.1"
}
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
@ -20898,6 +21180,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-my-way": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz",
"integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-querystring": "^1.0.0",
"safe-regex2": "^5.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -21768,9 +22065,9 @@
"license": "ISC"
},
"node_modules/grammex": {
"version": "3.1.12",
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz",
"integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==",
"version": "3.1.13",
"resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz",
"integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==",
"devOptional": true,
"license": "MIT"
},
@ -22645,9 +22942,8 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"devOptional": true,
"license": "ISC",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
}
@ -29188,17 +29484,17 @@
}
},
"node_modules/prisma": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz",
"integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz",
"integrity": "sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "7.8.0",
"@prisma/dev": "0.24.3",
"@prisma/engines": "7.8.0",
"@prisma/studio-core": "0.27.3",
"@prisma/config": "7.9.1",
"@prisma/dev": "0.24.17",
"@prisma/engines": "7.9.1",
"@prisma/studio-core": "0.33.0",
"mysql2": "3.15.3",
"postgres": "3.4.7"
},
@ -30152,6 +30448,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ret": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
"integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -30183,9 +30489,8 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
"integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
"license": "Unlicense",
"optional": true,
"peer": true
"devOptional": true,
"license": "Unlicense"
},
"node_modules/rolldown": {
"version": "1.0.0-rc.4",
@ -30532,6 +30837,29 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-regex2": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz",
"integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==",
"devOptional": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"ret": "~0.5.0"
},
"bin": {
"safe-regex2": "bin/safe-regex2.js"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -34374,9 +34702,9 @@
"license": "MIT"
},
"node_modules/valibot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz",
"integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {

10
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.37.0",
"version": "3.38.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -87,8 +87,8 @@
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "3.0.0",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",
"@prisma/adapter-pg": "7.9.1",
"@prisma/client": "7.9.1",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1",
"ai": "7.0.37",
@ -171,7 +171,7 @@
"@nx/storybook": "23.0.2",
"@nx/web": "23.0.2",
"@nx/workspace": "23.0.2",
"@prisma/config": "7.8.0",
"@prisma/config": "7.9.1",
"@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10",
"@storybook/addon-themes": "10.1.10",
@ -201,7 +201,7 @@
"nx": "23.0.2",
"prettier": "3.9.6",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.8.0",
"prisma": "7.9.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "8.4.0",

Loading…
Cancel
Save