diff --git a/CHANGELOG.md b/CHANGELOG.md index 54bf28b49..8e6752795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,53 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.25.0 - 2026-07-12 + +### Changed + +- Changed the default value of the `DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS` environment variable from `RAPID_API` to `MANUAL` +- Improved the language localization for Dutch (`nl`) +- Upgraded `helmet` from version `7.0.0` to `8.2.0` ### Fixed -- Fixed the misleading `netPerformancePercentWithCurrencyEffect` for reopened multi-trade positions by calculating it against the net invested capital +- Fixed the layout of the page tabs component by truncating long labels +- Fixed the display of assets without a currency in the search results of the assistant +- Fixed the display of assets without a currency in the symbol autocomplete component + +### Todo + +- **Breaking Change**: Set the environment variable `DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS=RAPID_API` to keep using _Rapid API_ as the data source of the _Fear & Greed Index_ (market mood) + +## 3.24.0 - 2026-07-11 + +### Added + +- Exposed the `DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS` environment variable to set the data source of the _Fear & Greed Index_ (market mood) +- Exposed the `ENABLE_FEATURE_RATE_LIMITING` environment variable to control rate limiting for authentication and sign-up endpoints +- Exposed the `TRUST_PROXY` environment variable to determine the client IP address when running behind a reverse proxy + +### Changed + +- Rounded the value of the _Fear & Greed Index_ (market mood) +- Improved the language localization for Korean (`ko`) + +## 3.23.0 - 2026-07-10 + +### Changed + +- Migrated the deprecated `@nx/webpack:webpack` executor to `@nx/webpack/plugin` +- Set the change detection strategy to `OnPush` in the about page +- Set the change detection strategy to `OnPush` in the admin control panel +- Set the change detection strategy to `OnPush` in the blog page components +- Set the change detection strategy to `OnPush` in the Frequently Asked Questions (FAQ) page +- Set the change detection strategy to `OnPush` in the home page +- Set the change detection strategy to `OnPush` in the markets overview +- Set the change detection strategy to `OnPush` in the resources page +- Set the change detection strategy to `OnPush` in the user account page +- Set the change detection strategy to `OnPush` in the _Zen Mode_ +- Improved the language localization for Chinese (`zh`) +- Improved the language localization for German (`de`) ## 3.22.0 - 2026-07-08 diff --git a/README.md b/README.md index a9b3a3055..ce89412a3 100644 --- a/README.md +++ b/README.md @@ -85,27 +85,28 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c ### Supported Environment Variables -| Name | Type | Default Value | Description | -| --------------------------- | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | -| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | -| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | -| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | -| `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) | -| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token | -| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | -| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | -| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | -| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | -| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | -| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | -| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | -| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | -| `REDIS_HOST` | `string` | | The host where _Redis_ is running | -| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | -| `REDIS_PORT` | `number` | | The port where _Redis_ is running | -| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | -| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | +| Name | Type | Default Value | Description | +| --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | +| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | +| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | +| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | +| `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) | +| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token | +| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | +| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | +| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | +| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | +| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | +| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | +| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | +| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | +| `REDIS_HOST` | `string` | | The host where _Redis_ is running | +| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | +| `REDIS_PORT` | `number` | | The port where _Redis_ is running | +| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | +| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | +| `TRUST_PROXY` | `string` (optional) | | The [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting of _Express.js_ to determine the client IP address for rate limiting, e.g. `1` if the Ghostfolio application runs behind a single reverse proxy | #### OpenID Connect OIDC (experimental) diff --git a/apps/api/project.json b/apps/api/project.json index 4e1affb13..c4c1be7ac 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -7,32 +7,10 @@ "generators": {}, "targets": { "build": { - "executor": "@nx/webpack:webpack", - "options": { - "compiler": "tsc", - "deleteOutputPath": false, - "main": "apps/api/src/main.ts", - "outputPath": "dist/apps/api", - "sourceMap": true, - "target": "node", - "tsConfig": "apps/api/tsconfig.app.json", - "webpackConfig": "apps/api/webpack.config.js" - }, "configurations": { - "production": { - "generatePackageJson": true, - "optimization": true, - "extractLicenses": true, - "inspect": false, - "fileReplacements": [ - { - "replace": "apps/api/src/environments/environment.ts", - "with": "apps/api/src/environments/environment.prod.ts" - } - ] - } + "production": {} }, - "outputs": ["{options.outputPath}"] + "outputs": ["{workspaceRoot}/dist/apps/api"] }, "copy-assets": { "executor": "nx:run-commands", diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 04b1f3bf2..4bdd50c9e 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,7 +1,9 @@ import { EventsModule } from '@ghostfolio/api/events/events.module'; +import { getRedisConnectionOptions } from '@ghostfolio/api/helper/redis.helper'; import { BullBoardAuthMiddleware } from '@ghostfolio/api/middlewares/bull-board-auth.middleware'; import { HtmlTemplateMiddleware } from '@ghostfolio/api/middlewares/html-template.middleware'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CronModule } from '@ghostfolio/api/services/cron/cron.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; @@ -13,18 +15,22 @@ import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/po import { BULL_BOARD_ROUTE, DEFAULT_LANGUAGE_CODE, - SUPPORTED_LANGUAGE_CODES + SUPPORTED_LANGUAGE_CODES, + THROTTLE_DEFAULT_LIMIT, + THROTTLE_DEFAULT_TTL } from '@ghostfolio/common/config'; import { ExpressAdapter } from '@bull-board/express'; import { BullBoardModule } from '@bull-board/nestjs'; +import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; import { BullModule } from '@nestjs/bull'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; -import { StatusCodes } from 'http-status-codes'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { join } from 'node:path'; import { AccessModule } from './access/access.module'; @@ -95,12 +101,13 @@ import { UserModule } from './user/user.module'; middleware: BullBoardAuthMiddleware, route: BULL_BOARD_ROUTE }), - BullModule.forRoot({ - redis: { - db: parseInt(process.env.REDIS_DB ?? '0', 10), - host: process.env.REDIS_HOST, - password: process.env.REDIS_PASSWORD, - port: parseInt(process.env.REDIS_PORT ?? '6379', 10) + BullModule.forRootAsync({ + imports: [ConfigurationModule], + inject: [ConfigurationService], + useFactory: (configurationService: ConfigurationService) => { + return { + redis: getRedisConnectionOptions(configurationService) + }; } }), CacheModule, @@ -168,6 +175,36 @@ import { UserModule } from './user/user.module'; SubscriptionModule, SymbolModule, TagsModule, + ThrottlerModule.forRootAsync({ + imports: [ConfigurationModule], + inject: [ConfigurationService], + useFactory: (configurationService: ConfigurationService) => { + const isRateLimitingEnabled = configurationService.get( + 'ENABLE_FEATURE_RATE_LIMITING' + ); + + return { + errorMessage: getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + skipIf: () => { + return !isRateLimitingEnabled; + }, + storage: isRateLimitingEnabled + ? new ThrottlerStorageRedisService({ + ...getRedisConnectionOptions(configurationService), + // Reject commands immediately while Redis is unavailable + enableOfflineQueue: false, + maxRetriesPerRequest: 1 + }) + : undefined, + throttlers: [ + { + limit: THROTTLE_DEFAULT_LIMIT, + ttl: THROTTLE_DEFAULT_TTL + } + ] + }; + } + }), UserModule, WatchlistModule ], diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index 388f1dbd3..ac50f4b8a 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -1,4 +1,5 @@ import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; +import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; @@ -39,6 +40,7 @@ export class AuthController { * @deprecated */ @Get('anonymous/:accessToken') + @UseGuards(CustomThrottlerGuard) public async accessTokenLoginGet( @Param('accessToken') accessToken: string ): Promise { @@ -55,6 +57,7 @@ export class AuthController { } @Post('anonymous') + @UseGuards(CustomThrottlerGuard) public async accessTokenLogin( @Body() body: { accessToken: string } ): Promise { @@ -135,6 +138,7 @@ export class AuthController { } @Post('webauthn/generate-authentication-options') + @UseGuards(CustomThrottlerGuard) public async generateAuthenticationOptions( @Body() body: { deviceId: string } ) { @@ -156,6 +160,7 @@ export class AuthController { } @Post('webauthn/verify-authentication') + @UseGuards(CustomThrottlerGuard) public async verifyAuthentication( @Body() body: { deviceId: string; credential: AssertionCredentialJSON } ) { diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts index 5dad0511f..105bb0780 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts @@ -1,11 +1,11 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, - ghostfolioFearAndGreedIndexDataSourceStocks, ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config'; @@ -36,6 +36,7 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes'; @Controller('market-data') export class MarketDataController { public constructor( + private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly symbolProfileService: SymbolProfileService, @@ -64,7 +65,8 @@ export class MarketDataController { this.symbolService.get({ includeHistoricalData, dataGatheringItem: { - dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), symbol: ghostfolioFearAndGreedIndexSymbolStocks }, useIntradayData: true diff --git a/apps/api/src/app/endpoints/market-data/market-data.module.ts b/apps/api/src/app/endpoints/market-data/market-data.module.ts index 1de10907b..47ce11502 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.module.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.module.ts @@ -1,4 +1,5 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -8,6 +9,11 @@ import { MarketDataController } from './market-data.controller'; @Module({ controllers: [MarketDataController], - imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule] + imports: [ + DataProviderModule, + MarketDataServiceModule, + SymbolModule, + SymbolProfileModule + ] }) export class MarketDataModule {} diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 4441036a2..10836f7b8 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -4,6 +4,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { @@ -15,8 +16,7 @@ import { PROPERTY_GITHUB_STARGAZERS, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, - PROPERTY_UPTIME, - ghostfolioFearAndGreedIndexDataSourceStocks + PROPERTY_UPTIME } from '@ghostfolio/common/config'; import { InfoItem, Statistics } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -33,6 +33,7 @@ export class InfoService { public constructor( private readonly benchmarkService: BenchmarkService, private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly jwtService: JwtService, private readonly propertyService: PropertyService, @@ -60,13 +61,15 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + const fearAndGreedIndexDataSource = + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(); + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { info.fearAndGreedDataSource = encodeDataSource( - ghostfolioFearAndGreedIndexDataSourceStocks + fearAndGreedIndexDataSource ); } else { - info.fearAndGreedDataSource = - ghostfolioFearAndGreedIndexDataSourceStocks; + info.fearAndGreedDataSource = fearAndGreedIndexDataSource; } globalPermissions.push(permissions.enableFearAndGreedIndex); diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index ab3f76703..e73e222ac 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -141,9 +141,9 @@ export abstract class PortfolioCalculator { } return { - SymbolProfile, tags, type, + assetProfile: SymbolProfile, date: format(date, DATE_FORMAT), fee: new Big(feeInAssetProfileCurrency), feeInBaseCurrency: new Big(feeInBaseCurrency), @@ -934,23 +934,23 @@ export abstract class PortfolioCalculator { let lastTransactionPoint: TransactionPoint = null; for (const { + assetProfile, date, fee, feeInBaseCurrency, quantity, - SymbolProfile, tags, type, unitPrice } of this.activities) { let currentTransactionPointItem: TransactionPointSymbol; - const assetSubClass = SymbolProfile.assetSubClass; - const currency = SymbolProfile.currency; - const dataSource = SymbolProfile.dataSource; + const assetSubClass = assetProfile.assetSubClass; + const currency = assetProfile.currency; + const dataSource = assetProfile.dataSource; const factor = getFactor(type); - const skipErrors = !!SymbolProfile.userId; // Skip errors for custom asset profiles - const symbol = SymbolProfile.symbol; + const skipErrors = !!assetProfile.userId; // Skip errors for custom asset profiles + const symbol = assetProfile.symbol; const oldAccumulatedSymbol = symbols[symbol]; @@ -1034,12 +1034,12 @@ export abstract class PortfolioCalculator { 'id' ); - symbols[SymbolProfile.symbol] = currentTransactionPointItem; + symbols[symbol] = currentTransactionPointItem; const items = lastTransactionPoint?.items ?? []; const newItems = items.filter(({ symbol }) => { - return symbol !== SymbolProfile.symbol; + return symbol !== assetProfile.symbol; }); newItems.push(currentTransactionPointItem); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 75eacc1e5..594f17618 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -193,12 +193,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // Clone orders to keep the original values in this.orders let orders: PortfolioOrderItem[] = cloneDeep( - this.activities.filter(({ SymbolProfile }) => { - return SymbolProfile.symbol === symbol; + this.activities.filter(({ assetProfile }) => { + return assetProfile.symbol === symbol; }) ); - const isCash = orders[0]?.SymbolProfile?.assetSubClass === 'CASH'; + const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH'; if (orders.length <= 0) { return { @@ -300,32 +300,30 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }; } + const assetProfile: PortfolioOrderItem['assetProfile'] = { + dataSource, + symbol, + assetSubClass: isCash ? 'CASH' : undefined + }; + // Add a synthetic order at the start and the end date orders.push({ + assetProfile, date: startDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), itemType: 'start', quantity: new Big(0), - SymbolProfile: { - dataSource, - symbol, - assetSubClass: isCash ? 'CASH' : undefined - }, type: 'BUY', unitPrice: unitPriceAtStartDate }); orders.push({ + assetProfile, date: endDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), itemType: 'end', - SymbolProfile: { - dataSource, - symbol, - assetSubClass: isCash ? 'CASH' : undefined - }, quantity: new Big(0), type: 'BUY', unitPrice: unitPriceAtEndDate @@ -358,15 +356,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } else { orders.push({ + assetProfile, date: dateString, fee: new Big(0), feeInBaseCurrency: new Big(0), quantity: new Big(0), - SymbolProfile: { - dataSource, - symbol, - assetSubClass: isCash ? 'CASH' : undefined - }, type: 'BUY', unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, unitPriceFromMarketData: diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 2dbd68f12..8d90c5bc6 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -1,13 +1,13 @@ import { Activity } from '@ghostfolio/common/interfaces'; export interface PortfolioOrder extends Pick { + assetProfile: Pick< + Activity['SymbolProfile'], + 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' + >; date: string; fee: Big; feeInBaseCurrency: Big; quantity: Big; - SymbolProfile: Pick< - Activity['SymbolProfile'], - 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' - >; unitPrice: Big; } diff --git a/apps/api/src/app/redis-cache/redis-cache.module.ts b/apps/api/src/app/redis-cache/redis-cache.module.ts index d0e3228b7..8d56c7c51 100644 --- a/apps/api/src/app/redis-cache/redis-cache.module.ts +++ b/apps/api/src/app/redis-cache/redis-cache.module.ts @@ -1,3 +1,4 @@ +import { getRedisConnectionUrl } from '@ghostfolio/api/helper/redis.helper'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -14,16 +15,8 @@ import { RedisCacheService } from './redis-cache.service'; imports: [ConfigurationModule], inject: [ConfigurationService], useFactory: async (configurationService: ConfigurationService) => { - const redisPassword = encodeURIComponent( - configurationService.get('REDIS_PASSWORD') - ); - return { - stores: [ - createKeyv( - `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}` - ) - ], + stores: [createKeyv(getRedisConnectionUrl(configurationService))], ttl: configurationService.get('CACHE_TTL') }; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 6346ce43a..7a1fc71dc 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -1,11 +1,16 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { + HEADER_KEY_IMPERSONATION, + THROTTLE_SIGNUP_LIMIT, + THROTTLE_SIGNUP_TTL +} from '@ghostfolio/common/config'; import { DeleteOwnUserDto, UpdateOwnAccessTokenDto, @@ -37,6 +42,7 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; +import { Throttle } from '@nestjs/throttler'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { merge, size } from 'lodash'; @@ -128,6 +134,13 @@ export class UserController { } @Post() + @Throttle({ + default: { + limit: THROTTLE_SIGNUP_LIMIT, + ttl: THROTTLE_SIGNUP_TTL + } + }) + @UseGuards(CustomThrottlerGuard) public async signupUser(): Promise { const isUserSignupEnabled = await this.propertyService.isUserSignupEnabled(); diff --git a/apps/api/src/guards/custom-throttler.guard.ts b/apps/api/src/guards/custom-throttler.guard.ts new file mode 100644 index 000000000..c4f0e806d --- /dev/null +++ b/apps/api/src/guards/custom-throttler.guard.ts @@ -0,0 +1,21 @@ +import { ExecutionContext, Injectable, Logger } from '@nestjs/common'; +import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler'; + +@Injectable() +export class CustomThrottlerGuard extends ThrottlerGuard { + private readonly logger = new Logger(CustomThrottlerGuard.name); + + public async canActivate(context: ExecutionContext): Promise { + try { + return await super.canActivate(context); + } catch (error) { + if (error instanceof ThrottlerException) { + throw error; + } + + this.logger.error(error); + + return true; + } + } +} diff --git a/apps/api/src/helper/redis.helper.ts b/apps/api/src/helper/redis.helper.ts new file mode 100644 index 000000000..81fe905eb --- /dev/null +++ b/apps/api/src/helper/redis.helper.ts @@ -0,0 +1,22 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; + +export function getRedisConnectionOptions( + configurationService: ConfigurationService +) { + return { + db: configurationService.get('REDIS_DB'), + host: configurationService.get('REDIS_HOST'), + password: configurationService.get('REDIS_PASSWORD'), + port: configurationService.get('REDIS_PORT') + }; +} + +export function getRedisConnectionUrl( + configurationService: ConfigurationService +): string { + const { db, host, password, port } = + getRedisConnectionOptions(configurationService); + const encodedPassword = encodeURIComponent(password); + + return `redis://${encodedPassword ? `:${encodedPassword}` : ''}@${host}:${port}/${db}`; +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 63185a48b..33ad032e9 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,3 +1,4 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { BULL_BOARD_ROUTE, DEFAULT_HOST, @@ -39,6 +40,8 @@ async function bootstrap() { ) as LogLevel[]; } catch {} + await configApp.close(); + const app = await NestFactory.create(AppModule, { logger: customLogLevels ?? @@ -97,6 +100,23 @@ async function bootstrap() { }); } + const configurationService = app.get(ConfigurationService); + + const trustProxy = configurationService.get('TRUST_PROXY'); + + if (trustProxy) { + app.set('trust proxy', trustProxy); + } + + if ( + configurationService.get('ENABLE_FEATURE_RATE_LIMITING') && + trustProxy === '' + ) { + logger.warn( + 'Rate limiting is enabled, but TRUST_PROXY is not set. If the Ghostfolio application runs behind a reverse proxy, the rate limits are shared across all clients.' + ); + } + const HOST = configService.get('HOST') || DEFAULT_HOST; const PORT = configService.get('PORT') || DEFAULT_PORT; diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index c96ccd946..3c152d5df 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -13,9 +13,31 @@ import { import { Injectable } from '@nestjs/common'; import { DataSource } from '@prisma/client'; -import { bool, cleanEnv, host, json, num, port, str, url } from 'envalid'; +import { + bool, + cleanEnv, + host, + json, + makeValidator, + num, + port, + str, + url +} from 'envalid'; import ms from 'ms'; +const trustProxy = makeValidator((input) => { + if (/^\d+$/.test(input)) { + return Number(input); + } else if (input === 'false') { + return false; + } else if (input === 'true') { + return true; + } + + return input; +}); + @Injectable() export class ConfigurationService { private readonly environmentConfiguration: Environment; @@ -34,6 +56,9 @@ export class ConfigurationService { CACHE_QUOTES_TTL: num({ default: ms('1 minute') }), CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), + DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: str({ + default: DataSource.MANUAL + }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCES: json({ default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] @@ -47,6 +72,7 @@ export class ConfigurationService { ENABLE_FEATURE_CRON: bool({ default: true }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }), + ENABLE_FEATURE_RATE_LIMITING: bool({ default: false }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), ENABLE_FEATURE_STATISTICS: bool({ default: false }), ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }), @@ -111,6 +137,7 @@ export class ConfigurationService { default: environment.rootUrl }), STRIPE_SECRET_KEY: str({ default: '' }), + TRUST_PROXY: trustProxy({ default: '' }), TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }), TWITTER_API_KEY: str({ default: 'dummyApiKey' }), diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index c6e9c83c1..8723466b0 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -177,6 +177,12 @@ export class DataProviderService implements OnModuleInit { ]; } + public getDataSourceForFearAndGreedIndexStocks(): DataSource { + return DataSource[ + this.configurationService.get('DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS') + ]; + } + public getDataSourceForImport(): DataSource { return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')]; } diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index d8ba1ec67..e704f2861 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -64,13 +64,15 @@ export class RapidApiService implements DataProviderInterface { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { const fgi = await this.getFearAndGreedIndex(); - return { - [symbol]: { - [format(getYesterday(), DATE_FORMAT)]: { - marketPrice: fgi.previousClose.value + if (fgi) { + return { + [symbol]: { + [format(getYesterday(), DATE_FORMAT)]: { + marketPrice: fgi.previousClose.value + } } - } - }; + }; + } } } catch (error) { throw new Error( @@ -101,14 +103,16 @@ export class RapidApiService implements DataProviderInterface { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { const fgi = await this.getFearAndGreedIndex(); - return { - [symbol]: { - currency: undefined, - dataSource: this.getName(), - marketPrice: fgi.now.value, - marketState: 'open' - } - }; + if (fgi) { + return { + [symbol]: { + currency: undefined, + dataSource: this.getName(), + marketPrice: fgi.now.value, + marketState: 'open' + } + }; + } } } catch (error) { this.logger.error(error); diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 89463e201..bb58b01ab 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -13,6 +13,7 @@ export interface Environment extends CleanedEnvAccessors { CACHE_QUOTES_TTL: number; CACHE_TTL: number; DATA_SOURCE_EXCHANGE_RATES: string; + DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: string; DATA_SOURCE_IMPORT: string; DATA_SOURCES: string[]; DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; @@ -22,6 +23,7 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_CRON: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean; + ENABLE_FEATURE_RATE_LIMITING: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean; ENABLE_FEATURE_STATISTICS: boolean; ENABLE_FEATURE_SUBSCRIPTION: boolean; @@ -56,6 +58,7 @@ export interface Environment extends CleanedEnvAccessors { REQUEST_TIMEOUT: number; ROOT_URL: string; STRIPE_SECRET_KEY: string; + TRUST_PROXY: boolean | number | string; TWITTER_ACCESS_TOKEN: string; TWITTER_ACCESS_TOKEN_SECRET: string; TWITTER_API_KEY: string; diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts index 80d53169c..bdb1a7988 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.module.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts @@ -1,13 +1,19 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; import { Module } from '@nestjs/common'; @Module({ exports: [TwitterBotService], - imports: [BenchmarkModule, ConfigurationModule, SymbolModule], + imports: [ + BenchmarkModule, + ConfigurationModule, + DataProviderModule, + SymbolModule + ], providers: [TwitterBotService] }) export class TwitterBotModule {} diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index 366b016b6..8dffddf7b 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -1,10 +1,8 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { - ghostfolioFearAndGreedIndexDataSourceStocks, - ghostfolioFearAndGreedIndexSymbolStocks -} from '@ghostfolio/common/config'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config'; import { resolveFearAndGreedIndex, resolveMarketCondition @@ -24,6 +22,7 @@ export class TwitterBotService implements OnModuleInit { public constructor( private readonly benchmarkService: BenchmarkService, private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, private readonly symbolService: SymbolService ) {} @@ -49,7 +48,8 @@ export class TwitterBotService implements OnModuleInit { try { const symbolItem = await this.symbolService.get({ dataGatheringItem: { - dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), symbol: ghostfolioFearAndGreedIndexSymbolStocks } }); diff --git a/apps/api/webpack.config.js b/apps/api/webpack.config.js index 2cc38b985..e59eee0bf 100644 --- a/apps/api/webpack.config.js +++ b/apps/api/webpack.config.js @@ -1,6 +1,49 @@ -const { composePlugins, withNx } = require('@nx/webpack'); +const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); +const path = require('path'); -module.exports = composePlugins(withNx(), (config, { options, context }) => { - // Customize webpack config here - return config; +// These options were migrated by @nx/webpack:convert-to-inferred from +// the project.json file and merged with the options in this file +const configValues = { + build: { + default: { + compiler: 'tsc', + deleteOutputPath: false, + main: './src/main.ts', + outputPath: 'dist/apps/api', + outputHashing: 'none', + sourceMap: true, + target: 'node', + tsConfig: './tsconfig.app.json' + }, + production: { + extractLicenses: true, + fileReplacements: [ + { + replace: path.resolve(__dirname, './src/environments/environment.ts'), + with: path.resolve( + __dirname, + './src/environments/environment.prod.ts' + ) + } + ], + generatePackageJson: true, + inspect: false, + optimization: true + } + } +}; + +// Determine the correct configValue to use based on the configuration +const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + +const buildOptions = { + ...configValues.build.default, + ...configValues.build[configuration] +}; + +/** + * @type {import('webpack').WebpackOptionsNormalized} + */ +module.exports = async () => ({ + plugins: [new NxAppWebpackPlugin(buildOptions)] }); diff --git a/apps/client/src/app/app.routes.ts b/apps/client/src/app/app.routes.ts index 9588cee68..2c0591b2c 100644 --- a/apps/client/src/app/app.routes.ts +++ b/apps/client/src/app/app.routes.ts @@ -128,8 +128,7 @@ export const routes: Routes = [ import('./pages/webauthn/webauthn-page.component').then( (c) => c.GfWebauthnPageComponent ), - path: internalRoutes.webauthn.path, - title: internalRoutes.webauthn.title + path: internalRoutes.webauthn.path }, { path: internalRoutes.zen.path, diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.html b/apps/client/src/app/components/admin-settings/admin-settings.component.html index 8ee6250e5..643737c4d 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.html +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.html @@ -76,7 +76,7 @@ newpopular } diff --git a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html index 67274ae38..dd2925f43 100644 --- a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html +++ b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html @@ -5,7 +5,7 @@
{{ fearAndGreedIndexText }} {{ fearAndGreedIndex }}{{ fearAndGreedIndex | number: '1.0-0' }}/100
diff --git a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts index 32e2cc29a..b507f0008 100644 --- a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts +++ b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts @@ -1,6 +1,7 @@ import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; import { translate } from '@ghostfolio/ui/i18n'; +import { DecimalPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -11,7 +12,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgxSkeletonLoaderModule], + imports: [DecimalPipe, NgxSkeletonLoaderModule], selector: 'gf-fear-and-greed-index', styleUrls: ['./fear-and-greed-index.component.scss'], templateUrl: './fear-and-greed-index.component.html' diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index c12509d58..8e70c1a8c 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -22,6 +22,7 @@ import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { DataService } from '@ghostfolio/ui/services'; +import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, @@ -42,6 +43,7 @@ import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { MatToolbarModule } from '@angular/material/toolbar'; import { Router, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; +import { StatusCodes } from 'http-status-codes'; import { addIcons } from 'ionicons'; import { closeOutline, @@ -315,10 +317,15 @@ export class GfHeaderComponent implements OnChanges { this.dataService .loginAnonymous(data?.accessToken) .pipe( - catchError(() => { - this.notificationService.alert({ - title: $localize`Oops! Incorrect Security Token.` - }); + catchError((error: HttpErrorResponse) => { + if (error.status !== StatusCodes.TOO_MANY_REQUESTS) { + // The notification for too many requests is handled in the + // HttpResponseInterceptor + + this.notificationService.alert({ + title: $localize`Oops! Incorrect Security Token.` + }); + } return EMPTY; }), diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index a789f3c66..cf7e52833 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -20,6 +20,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -53,32 +54,34 @@ import { DeviceDetectorService } from 'ngx-device-detector'; export class GfHomeHoldingsComponent implements OnInit { public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; - public deviceType: string; - public hasImpersonationId: boolean; - public hasPermissionToAccessHoldingsChart: boolean; - public hasPermissionToCreateActivity: boolean; - public holdings: PortfolioPosition[]; - public holdingType: HoldingType = 'ACTIVE'; - public holdingTypeOptions: ToggleOption[] = [ + protected deviceType: string; + protected hasImpersonationId: boolean; + protected hasPermissionToAccessHoldingsChart: boolean; + protected hasPermissionToCreateActivity: boolean; + protected holdings: PortfolioPosition[]; + protected holdingType: HoldingType = 'ACTIVE'; + protected readonly holdingTypeOptions: ToggleOption[] = [ { label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Closed`, value: 'CLOSED' } ]; - public routerLinkPortfolioActivities = + protected readonly routerLinkPortfolioActivities = internalRoutes.portfolio.subRoutes.activities.routerLink; - public user: User; - public viewModeFormControl = new FormControl( + protected user: User; + protected readonly viewModeFormControl = new FormControl( GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE ); - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private impersonationStorageService: ImpersonationStorageService, - private router: Router, - private userService: UserService - ) { + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public constructor() { addIcons({ gridOutline, reorderFourOutline }); } @@ -119,6 +122,10 @@ export class GfHomeHoldingsComponent implements OnInit { this.viewModeFormControl.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((holdingsViewMode) => { + if (!holdingsViewMode) { + return; + } + this.dataService .putUserSetting({ holdingsViewMode }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -135,13 +142,13 @@ export class GfHomeHoldingsComponent implements OnInit { }); } - public onChangeHoldingType(aHoldingType: HoldingType) { + protected onChangeHoldingType(aHoldingType: HoldingType) { this.holdingType = aHoldingType; this.initialize(); } - public onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { + protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { if (dataSource && symbol) { this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } @@ -185,7 +192,7 @@ export class GfHomeHoldingsComponent implements OnInit { ); } - this.holdings = undefined; + this.holdings = []; this.fetchHoldings() .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index 40b8f145b..6bf99b31d 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -14,6 +14,7 @@ import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { DataService } from '@ghostfolio/ui/services'; import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, @@ -27,6 +28,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ GfBenchmarkComponent, GfFearAndGreedIndexComponent, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts index 3d2760202..b1e32c6c3 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts @@ -8,10 +8,10 @@ import { GfValueComponent } from '@ghostfolio/ui/value'; import { ChangeDetectionStrategy, Component, - EventEmitter, + inject, Input, OnChanges, - Output + output } from '@angular/core'; import { MatTooltipModule } from '@angular/material/tooltip'; import { IonIcon } from '@ionic/angular/standalone'; @@ -40,44 +40,46 @@ export class GfPortfolioSummaryComponent implements OnChanges { @Input() summary: PortfolioSummary; @Input() user: User; - @Output() emergencyFundChanged = new EventEmitter(); + public emergencyFundChanged = output(); - public buyAndSellActivitiesTooltip = translate( + protected readonly buyAndSellActivitiesTooltip = translate( 'BUY_AND_SELL_ACTIVITIES_TOOLTIP' ); - public precision = 2; - public timeInMarket: string; + protected precision = 2; + protected timeInMarket: string | undefined; - public get buyingPowerPercentage() { + private readonly notificationService = inject(NotificationService); + + public constructor() { + addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline }); + } + + protected get buyingPowerPercentage() { return this.summary?.totalValueInBaseCurrency ? this.summary.cash / this.summary.totalValueInBaseCurrency : 0; } - public get emergencyFundPercentage() { + protected get emergencyFundPercentage() { return this.summary?.totalValueInBaseCurrency ? (this.summary.emergencyFund?.total || 0) / this.summary.totalValueInBaseCurrency : 0; } - public get excludedFromAnalysisPercentage() { + protected get excludedFromAnalysisPercentage() { return this.summary?.totalValueInBaseCurrency ? this.summary.excludedAccountsAndActivities / this.summary.totalValueInBaseCurrency : 0; } - public constructor(private notificationService: NotificationService) { - addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline }); - } - public ngOnChanges() { if (this.summary) { if ( this.deviceType === 'mobile' && - this.summary.totalValueInBaseCurrency >= + (this.summary.totalValueInBaseCurrency ?? 0) >= NUMERICAL_PRECISION_THRESHOLD_6_FIGURES ) { this.precision = 0; @@ -98,7 +100,7 @@ export class GfPortfolioSummaryComponent implements OnChanges { } } - public onEditEmergencyFund() { + protected onEditEmergencyFund() { this.notificationService.prompt({ confirmFn: (value) => { const emergencyFund = parseFloat(value.trim()) || 0; diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts index bdf2c3eb6..74ee2f64a 100644 --- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts +++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts @@ -3,7 +3,6 @@ import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; @@ -14,7 +13,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - DestroyRef + DestroyRef, + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; @@ -40,29 +40,29 @@ import { catchError } from 'rxjs/operators'; templateUrl: './user-account-membership.html' }) export class GfUserAccountMembershipComponent { - public baseCurrency: string; - public coupon: number; - public couponId: string; - public defaultDateFormat: string; - public durationExtension: StringValue; - public hasPermissionForSubscription: boolean; - public hasPermissionToCreateApiKey: boolean; - public hasPermissionToUpdateUserSettings: boolean; - public price: number; - public priceId: string; - public routerLinkPricing = publicRoutes.pricing.routerLink; - public trySubscriptionMail = + protected readonly baseCurrency: string; + protected coupon: number | undefined; + protected defaultDateFormat: string; + protected durationExtension: StringValue | undefined; + protected readonly hasPermissionForSubscription: boolean; + protected hasPermissionToCreateApiKey: boolean; + protected hasPermissionToUpdateUserSettings: boolean; + protected price: number; + protected readonly trySubscriptionMail = 'mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=Hello%0D%0DI am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?%0D%0DKind regards'; - public user: User; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private notificationService: NotificationService, - private snackBar: MatSnackBar, - private userService: UserService - ) { + protected user: User; + + private couponId: string | undefined; + private priceId: string; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly notificationService = inject(NotificationService); + private readonly snackBar = inject(MatSnackBar); + private readonly userService = inject(UserService); + + public constructor() { const { baseCurrency, globalPermissions } = this.dataService.fetchInfo(); this.baseCurrency = baseCurrency; @@ -104,7 +104,7 @@ export class GfUserAccountMembershipComponent { }); } - public onCheckout() { + protected onCheckout() { this.dataService .createStripeCheckoutSession({ couponId: this.couponId, @@ -125,7 +125,7 @@ export class GfUserAccountMembershipComponent { }); } - public onGenerateApiKey() { + protected onGenerateApiKey() { this.notificationService.confirm({ confirmFn: () => { this.dataService @@ -158,7 +158,7 @@ export class GfUserAccountMembershipComponent { }); } - public onRedeemCoupon() { + protected onRedeemCoupon() { this.notificationService.prompt({ confirmFn: (value) => { const couponCode = value?.trim(); diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index c8210b0c3..c9626b188 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -20,11 +20,12 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { - FormBuilder, + NonNullableFormBuilder, FormsModule, ReactiveFormsModule, Validators @@ -69,22 +70,22 @@ import { catchError } from 'rxjs/operators'; templateUrl: './user-account-settings.html' }) export class GfUserAccountSettingsComponent implements OnInit { - public appearancePlaceholder = $localize`Auto`; - public baseCurrency: string; - public closeUserAccountMail: string; - public currencies: string[] = []; - public deleteOwnUserForm = this.formBuilder.group({ + protected readonly appearancePlaceholder = $localize`Auto`; + protected readonly baseCurrency: string; + protected closeUserAccountMail: string; + protected readonly currencies: string[] = []; + protected readonly deleteOwnUserForm = inject(NonNullableFormBuilder).group({ accessToken: ['', Validators.required] }); - public hasPermissionToDeleteOwnUser: boolean; - public hasPermissionToRequestOwnUserDeletion: boolean; - public hasPermissionToUpdateViewMode: boolean; - public hasPermissionToUpdateUserSettings: boolean; - public isAccessTokenHidden = true; - public isFingerprintSupported = this.doesBrowserSupportAuthn(); - public isWebAuthnEnabled: boolean; - public language = document.documentElement.lang; - public locales = [ + protected hasPermissionToDeleteOwnUser: boolean; + protected hasPermissionToRequestOwnUserDeletion: boolean; + protected hasPermissionToUpdateViewMode: boolean; + protected hasPermissionToUpdateUserSettings: boolean; + protected isAccessTokenHidden = true; + protected readonly isFingerprintSupported = this.doesBrowserSupportAuthn(); + protected isWebAuthnEnabled: boolean; + protected readonly language = document.documentElement.lang; + protected locales = [ 'ca', 'de', 'de-CH', @@ -102,19 +103,18 @@ export class GfUserAccountSettingsComponent implements OnInit { 'uk', 'zh' ]; - public user: User; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private formBuilder: FormBuilder, - private notificationService: NotificationService, - private settingsStorageService: SettingsStorageService, - private snackBar: MatSnackBar, - private userService: UserService, - public webAuthnService: WebAuthnService - ) { + protected user: User; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly notificationService = inject(NotificationService); + private readonly settingsStorageService = inject(SettingsStorageService); + private readonly snackBar = inject(MatSnackBar); + private readonly userService = inject(UserService); + private readonly webAuthnService = inject(WebAuthnService); + + public constructor() { const { baseCurrency, currencies } = this.dataService.fetchInfo(); this.baseCurrency = baseCurrency; @@ -148,7 +148,10 @@ export class GfUserAccountSettingsComponent implements OnInit { permissions.updateViewMode ); - this.locales.push(this.user.settings.locale); + if (this.user.settings.locale) { + this.locales.push(this.user.settings.locale); + } + this.locales = Array.from(new Set(this.locales)).sort(); this.changeDetectorRef.markForCheck(); @@ -162,11 +165,11 @@ export class GfUserAccountSettingsComponent implements OnInit { this.update(); } - public isCommunityLanguage() { + protected isCommunityLanguage() { return !['de', 'en'].includes(this.language); } - public onChangeUserSetting(aKey: string, aValue: string) { + protected onChangeUserSetting(aKey: string, aValue: string) { this.dataService .putUserSetting({ [aKey]: aValue }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -190,12 +193,12 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } - public onCloseAccount() { + protected onCloseAccount() { this.notificationService.confirm({ confirmFn: () => { this.dataService .deleteOwnUser({ - accessToken: this.deleteOwnUserForm.get('accessToken').value + accessToken: this.deleteOwnUserForm.controls.accessToken.value }) .pipe( catchError(() => { @@ -218,7 +221,7 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } - public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) { + protected onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) { this.dataService .putUserSetting({ isExperimentalFeatures: aEvent.checked }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -234,13 +237,13 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } - public onExport() { + protected onExport() { this.dataService .fetchExport() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { for (const activity of data.activities) { - delete activity.id; + delete (activity as Omit & { id?: string }).id; } downloadAsFile({ @@ -254,7 +257,7 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } - public onRestrictedViewChange(aEvent: MatSlideToggleChange) { + protected onRestrictedViewChange(aEvent: MatSlideToggleChange) { this.dataService .putUserSetting({ isRestrictedView: aEvent.checked }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -270,7 +273,7 @@ export class GfUserAccountSettingsComponent implements OnInit { }); } - public async onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) { + protected async onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) { if (aEvent.checked) { try { await this.registerDevice(); @@ -293,7 +296,7 @@ export class GfUserAccountSettingsComponent implements OnInit { } } - public onViewModeChange(aEvent: MatSlideToggleChange) { + protected onViewModeChange(aEvent: MatSlideToggleChange) { this.dataService .putUserSetting({ viewMode: aEvent.checked === true ? 'ZEN' : 'DEFAULT' }) .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index 73e09f612..04d89c5a6 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -9,7 +9,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, - Inject, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -49,28 +49,30 @@ import { templateUrl: './user-detail-dialog.html' }) export class GfUserDetailDialogComponent implements OnInit { - public baseCurrency: string; - public readonly getCountryName = getCountryName; - public subscriptionsDataSource = new MatTableDataSource(); - public subscriptionsDisplayedColumns = [ + protected readonly baseCurrency: string; + protected readonly getCountryName = getCountryName; + protected readonly subscriptionsDataSource = + new MatTableDataSource(); + protected readonly subscriptionsDisplayedColumns = [ 'createdAt', 'type', 'price', 'expiresAt' ]; - public user: AdminUserResponse; - - public constructor( - private adminService: AdminService, - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, - private dataService: DataService, - private destroyRef: DestroyRef, - public dialogRef: MatDialogRef< - GfUserDetailDialogComponent, - UserDetailDialogResult - > - ) { + protected user: AdminUserResponse; + + protected readonly data = inject(MAT_DIALOG_DATA); + + private readonly adminService = inject(AdminService); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly dialogRef = + inject>( + MatDialogRef + ); + + public constructor() { this.baseCurrency = this.dataService.fetchInfo().baseCurrency; addIcons({ @@ -98,26 +100,26 @@ export class GfUserDetailDialogComponent implements OnInit { }); } - public deleteUser() { + protected deleteUser() { this.dialogRef.close({ action: 'delete', userId: this.data.userId }); } - public getSum() { + protected getSum() { return getSum( this.subscriptionsDataSource.data .filter(({ price }) => { return price !== null; }) .map(({ price }) => { - return new Big(price); + return new Big(price ?? 0); }) ).toNumber(); } - public getType({ createdAt, expiresAt, price }: Subscription) { + protected getType({ createdAt, expiresAt, price }: Subscription) { if (price) { return $localize`Paid`; } @@ -127,7 +129,7 @@ export class GfUserDetailDialogComponent implements OnInit { : $localize`Coupon`; } - public onClose() { + protected onClose() { this.dialogRef.close(); } } diff --git a/apps/client/src/app/core/http-response.interceptor.ts b/apps/client/src/app/core/http-response.interceptor.ts index 17927a924..7385e090c 100644 --- a/apps/client/src/app/core/http-response.interceptor.ts +++ b/apps/client/src/app/core/http-response.interceptor.ts @@ -98,15 +98,23 @@ export class HttpResponseInterceptor implements HttpInterceptor { }); } } else if (error.status === StatusCodes.TOO_MANY_REQUESTS) { - if (!this.snackBarRef) { - this.snackBarRef = this.snackBar.open( - $localize`Oops! It looks like you’re making too many requests. Please slow down a bit.` - ); + // Replace an already visible snack bar so that the rate limiting + // feedback is not swallowed + const snackBarRef = this.snackBar.open( + $localize`Oops! It looks like you’re making too many requests. Please slow down a bit.`, + undefined, + { + duration: ms('6 seconds') + } + ); - this.snackBarRef?.afterDismissed().subscribe(() => { + snackBarRef.afterDismissed().subscribe(() => { + if (this.snackBarRef === snackBarRef) { this.snackBarRef = undefined; - }); - } + } + }); + + this.snackBarRef = snackBarRef; } else if (error.status === StatusCodes.UNAUTHORIZED) { if (!error.url?.includes('/data-providers/ghostfolio/status')) { if (this.webAuthnService.isEnabled()) { diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 616977d80..615c0897d 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -8,7 +8,12 @@ import { } from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { @@ -21,6 +26,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-about-page', @@ -83,8 +89,6 @@ export class AboutPageComponent { }); this.user = state.user; - - this.changeDetectorRef.markForCheck(); } this.tabs.push({ @@ -92,6 +96,8 @@ export class AboutPageComponent { label: publicRoutes.about.subRoutes.ossFriends.title, routerLink: publicRoutes.about.subRoutes.ossFriends.routerLink }); + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index eb4ddb95c..15957d346 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -12,7 +12,12 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { Component, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { @@ -24,6 +29,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-admin-page', @@ -35,6 +41,7 @@ export class AdminPageComponent { private user: User; + private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly tokenStorageService = inject(TokenStorageService); private readonly userService = inject(UserService); @@ -45,6 +52,8 @@ export class AdminPageComponent { this.user = state?.user; this.initializeTabs(); + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts index 597c8d998..ac6ec5c9e 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hallo-ghostfolio-page', diff --git a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts index dd29cfd80..22fe85853 100644 --- a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hello-ghostfolio-page', diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts index 91b05896d..2ab59f5ce 100644 --- a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-first-months-in-open-source-page', diff --git a/apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts b/apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts index c410dd2b2..2f2409131 100644 --- a/apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-meets-internet-identity-page', diff --git a/apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts b/apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts index 812151600..f40170ba6 100644 --- a/apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-how-do-i-get-my-finances-in-order-page', diff --git a/apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts b/apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts index 9004ac0e2..2627f4134 100644 --- a/apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-500-stars-on-github-page', diff --git a/apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts b/apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts index fd34a6470..9e9fc2f8d 100644 --- a/apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hacktoberfest-2022-page', diff --git a/apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts b/apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts index dd13d5b4d..2b57cb09e 100644 --- a/apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/11/black-friday-2022/black-friday-2022-page.component.ts @@ -1,11 +1,12 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule], selector: 'gf-black-friday-2022-page', diff --git a/apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts b/apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts index 6e5a19f3c..4df232ea3 100644 --- a/apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-the-importance-of-tracking-your-personal-finances-page', diff --git a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts index 53d670184..fb3f226ca 100644 --- a/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page', diff --git a/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts index cb8dffcd3..3533af5c4 100644 --- a/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-meets-umbrel-page', diff --git a/apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts b/apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts index 60cc4177d..7a7d0197c 100644 --- a/apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-1000-stars-on-github-page', diff --git a/apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts index 5781674a6..de4d9a3c2 100644 --- a/apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-unlock-your-financial-potential-with-ghostfolio-page', diff --git a/apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts b/apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts index 6c7bb2ae2..79adc0e93 100644 --- a/apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/07/exploring-the-path-to-fire/exploring-the-path-to-fire-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-exploring-the-path-to-fire-page-page', diff --git a/apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts b/apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts index c5a9cf178..937212d0c 100644 --- a/apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-joins-oss-friends-page', diff --git a/apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts b/apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts index 197bc3e6b..3971b725b 100644 --- a/apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/09/ghostfolio-2/ghostfolio-2-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-2-page', diff --git a/apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts b/apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts index 1cf3f20a5..77d8f7ee0 100644 --- a/apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hacktoberfest-2023-page', diff --git a/apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts b/apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts index 09c13cfa2..a4503c99b 100644 --- a/apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/11/black-week-2023/black-week-2023-page.component.ts @@ -1,11 +1,12 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule], selector: 'gf-black-week-2023-page', diff --git a/apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts b/apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts index 7c8b37931..14ff67a04 100644 --- a/apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts +++ b/apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hacktoberfest-2023-debriefing-page', diff --git a/apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts b/apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts index 47f61adad..abfa39ffc 100644 --- a/apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts +++ b/apps/client/src/app/pages/blog/2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hacktoberfest-2024-page', diff --git a/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts index d15f081f8..38b7b80c4 100644 --- a/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts +++ b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts @@ -1,11 +1,12 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule], selector: 'gf-black-weeks-2024-page', diff --git a/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts index 72990ca47..88c70723d 100644 --- a/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts +++ b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-hacktoberfest-2025-page', diff --git a/apps/client/src/app/pages/blog/2025/11/black-weeks-2025/black-weeks-2025-page.component.ts b/apps/client/src/app/pages/blog/2025/11/black-weeks-2025/black-weeks-2025-page.component.ts index c5947abf4..b98be9c76 100644 --- a/apps/client/src/app/pages/blog/2025/11/black-weeks-2025/black-weeks-2025-page.component.ts +++ b/apps/client/src/app/pages/blog/2025/11/black-weeks-2025/black-weeks-2025-page.component.ts @@ -1,11 +1,12 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule], selector: 'gf-black-weeks-2025-page', diff --git a/apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts b/apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts index 63cd09d9c..e8e0c6da9 100644 --- a/apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts +++ b/apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts @@ -1,10 +1,11 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterModule } from '@angular/router'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [MatButtonModule, RouterModule], selector: 'gf-ghostfolio-3-page', diff --git a/apps/client/src/app/pages/blog/blog-page.component.ts b/apps/client/src/app/pages/blog/blog-page.component.ts index 7f2c56d2d..f3227e3b0 100644 --- a/apps/client/src/app/pages/blog/blog-page.component.ts +++ b/apps/client/src/app/pages/blog/blog-page.component.ts @@ -1,7 +1,11 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataService } from '@ghostfolio/ui/services'; -import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA +} from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; @@ -9,6 +13,7 @@ import { addIcons } from 'ionicons'; import { chevronForwardOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [IonIcon, MatCardModule, RouterModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/apps/client/src/app/pages/faq/faq-page.component.ts b/apps/client/src/app/pages/faq/faq-page.component.ts index 60081687a..17f70d138 100644 --- a/apps/client/src/app/pages/faq/faq-page.component.ts +++ b/apps/client/src/app/pages/faq/faq-page.component.ts @@ -6,11 +6,12 @@ import { } from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-faq-page', diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 8c5caab22..574286c82 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -9,6 +9,7 @@ import { } from '@ghostfolio/ui/page-tabs'; import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, @@ -25,6 +26,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-home-page', @@ -85,9 +87,9 @@ export class GfHomePageComponent implements OnInit { : internalRoutes.home.subRoutes.markets.routerLink } ]; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index ba17dbef3..4dbf6ad9f 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -20,7 +20,7 @@ import { ChangeDetectorRef, Component, DestroyRef, - Inject + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -75,42 +75,46 @@ import { ActivityType } from './types/activity-type.type'; templateUrl: 'create-or-update-activity-dialog.html' }) export class GfCreateOrUpdateActivityDialogComponent { - public activityForm: FormGroup; - - public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass) - .map((id) => { - return { id, label: translate(id) } as AssetClassSelectorOption; - }) - .sort((a, b) => { - return a.label.localeCompare(b.label); - }); + protected activityForm: FormGroup; - public assetSubClassOptions: AssetClassSelectorOption[] = []; - public currencies: string[] = []; - public currencyOfAssetProfile: string | undefined; - public currentMarketPrice: number | null = null; - public defaultDateFormat: string; - public defaultLookupItems: LookupItem[] = []; - public hasPermissionToCreateOwnTag: boolean | undefined; - public isLoading = false; - public isToday = isToday; - public mode: 'create' | 'update'; - public tagsAvailable: Tag[] = []; - public total = 0; - public typesTranslationMap = new Map(); - public Validators = Validators; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams, - private dataService: DataService, - private dateAdapter: DateAdapter, - private destroyRef: DestroyRef, - public dialogRef: MatDialogRef, - private formBuilder: FormBuilder, - @Inject(MAT_DATE_LOCALE) private locale: string, - private userService: UserService - ) { + protected readonly assetClassOptions: AssetClassSelectorOption[] = + Object.keys(AssetClass) + .map((id) => { + return { id, label: translate(id) } as AssetClassSelectorOption; + }) + .sort((a, b) => { + return a.label.localeCompare(b.label); + }); + + protected assetSubClassOptions: AssetClassSelectorOption[] = []; + protected currencies: string[] = []; + protected currencyOfAssetProfile: string | undefined; + protected currentMarketPrice: number | null = null; + protected defaultDateFormat: string; + protected defaultLookupItems: LookupItem[] = []; + protected hasPermissionToCreateOwnTag: boolean | undefined; + protected isLoading = false; + protected readonly isToday = isToday; + protected mode: 'create' | 'update'; + protected tagsAvailable: Tag[] = []; + protected total = 0; + protected readonly typesTranslationMap = new Map(); + protected readonly Validators = Validators; + + protected readonly data = + inject(MAT_DIALOG_DATA); + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly dateAdapter = inject>(DateAdapter); + private readonly destroyRef = inject(DestroyRef); + private readonly dialogRef = + inject>(MatDialogRef); + private readonly formBuilder = inject(FormBuilder); + private locale = inject(MAT_DATE_LOCALE); + private readonly userService = inject(UserService); + + public constructor() { addIcons({ calendarClearOutline, refreshOutline }); } @@ -138,7 +142,9 @@ export class GfCreateOrUpdateActivityDialogComponent { return !['CASH'].includes(assetProfile.assetSubClass); }) .sort((a, b) => { - return a.assetProfile.name?.localeCompare(b.assetProfile.name); + return (a.assetProfile.name ?? '').localeCompare( + b.assetProfile.name ?? '' + ); }) .map(({ assetProfile }) => { return { @@ -463,14 +469,14 @@ export class GfCreateOrUpdateActivityDialogComponent { } } - public applyCurrentMarketPrice() { + protected applyCurrentMarketPrice() { this.activityForm.patchValue({ currencyOfUnitPrice: this.activityForm.get('currency')?.value, unitPrice: this.currentMarketPrice }); } - public dateFilter(aDate: Date) { + protected dateFilter(aDate: Date) { if (!aDate) { return true; } @@ -478,11 +484,11 @@ export class GfCreateOrUpdateActivityDialogComponent { return isAfter(aDate, new Date(0)); } - public onCancel() { + protected onCancel() { this.dialogRef.close(); } - public async onSubmit() { + protected async onSubmit() { const activity: CreateOrderDto | UpdateOrderDto = { accountId: this.activityForm.get('accountId')?.value, assetClass: this.activityForm.get('assetClass')?.value, diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index c3dbe6cf2..4541009a0 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -151,7 +151,7 @@ export class GfImportActivitiesDialogComponent { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.holdings = sortBy(holdings, ({ assetProfile }) => { - return assetProfile.name.toLowerCase(); + return assetProfile.name?.toLowerCase(); }); this.assetProfileForm.controls.assetProfileIdentifier.enable(); diff --git a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts index cbbe2d29c..0265357bf 100644 --- a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts +++ b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts @@ -9,8 +9,8 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, - Inject, - ViewChild + inject, + viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -53,26 +53,28 @@ import { UserAccountRegistrationDialogParams } from './interfaces/interfaces'; templateUrl: 'user-account-registration-dialog.html' }) export class GfUserAccountRegistrationDialogComponent { - @ViewChild(MatStepper) stepper!: MatStepper; + protected readonly stepper = viewChild.required(MatStepper); - public accessToken: string; - public authToken: string; - public isCreateAccountButtonDisabled = true; - public isDisclaimerChecked = false; - public role: string; - public routerLinkAboutTermsOfService = + protected accessToken: string | undefined; + protected authToken: string; + protected isCreateAccountButtonDisabled = true; + protected isDisclaimerChecked = false; + protected role: string; + protected readonly routerLinkAboutTermsOfService = publicRoutes.about.subRoutes.termsOfService.routerLink; - public constructor( - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: UserAccountRegistrationDialogParams, - private dataService: DataService, - private destroyRef: DestroyRef - ) { + protected readonly data = + inject(MAT_DIALOG_DATA); + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + + public constructor() { addIcons({ arrowForwardOutline, checkmarkOutline, copyOutline }); } - public createAccount() { + protected createAccount() { this.dataService .postUser() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -81,17 +83,17 @@ export class GfUserAccountRegistrationDialogComponent { this.authToken = authToken; this.role = role; - this.stepper.next(); + this.stepper().next(); this.changeDetectorRef.markForCheck(); }); } - public enableCreateAccountButton() { + protected enableCreateAccountButton() { this.isCreateAccountButtonDisabled = false; } - public onChangeDislaimerChecked() { + protected onChangeDislaimerChecked() { this.isDisclaimerChecked = !this.isDisclaimerChecked; } } diff --git a/apps/client/src/app/pages/resources/resources-page.component.ts b/apps/client/src/app/pages/resources/resources-page.component.ts index 52980be85..d88aed4ba 100644 --- a/apps/client/src/app/pages/resources/resources-page.component.ts +++ b/apps/client/src/app/pages/resources/resources-page.component.ts @@ -4,7 +4,7 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { bookOutline, @@ -14,6 +14,7 @@ import { } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-resources-page', diff --git a/apps/client/src/app/pages/user-account/user-account-page.component.ts b/apps/client/src/app/pages/user-account/user-account-page.component.ts index ec7547b3c..486506675 100644 --- a/apps/client/src/app/pages/user-account/user-account-page.component.ts +++ b/apps/client/src/app/pages/user-account/user-account-page.component.ts @@ -6,12 +6,18 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-user-account-page', @@ -52,9 +58,9 @@ export class GfUserAccountPageComponent { routerLink: internalRoutes.account.subRoutes.access.routerLink } ]; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ diamondOutline, keyOutline, settingsOutline }); diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts index 8e7e58fd1..d988e1bb9 100644 --- a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts +++ b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts @@ -1,6 +1,5 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; -import { GfLogoComponent } from '@ghostfolio/ui/logo'; import { ChangeDetectorRef, @@ -15,7 +14,7 @@ import { Router } from '@angular/router'; @Component({ host: { class: 'page' }, - imports: [GfLogoComponent, MatButtonModule, MatProgressSpinnerModule], + imports: [MatButtonModule, MatProgressSpinnerModule], selector: 'gf-webauthn-page', styleUrls: ['./webauthn-page.scss'], templateUrl: './webauthn-page.html' diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.html b/apps/client/src/app/pages/webauthn/webauthn-page.html index 308a7096b..a5cce9700 100644 --- a/apps/client/src/app/pages/webauthn/webauthn-page.html +++ b/apps/client/src/app/pages/webauthn/webauthn-page.html @@ -1,12 +1,6 @@
-
- -
- @if (!hasError) {
diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index 4a897093c..0eb55ae26 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -6,12 +6,18 @@ import { TabConfiguration } from '@ghostfolio/ui/page-tabs'; -import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { addIcons } from 'ionicons'; import { albumsOutline, analyticsOutline } from 'ionicons/icons'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfPageTabsComponent], selector: 'gf-zen-page', @@ -44,9 +50,9 @@ export class GfZenPageComponent { } ]; this.user = state.user; - - this.changeDetectorRef.markForCheck(); } + + this.changeDetectorRef.markForCheck(); }); addIcons({ albumsOutline, analyticsOutline }); diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 3ef04ff96..c1d6f9d8b 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -599,7 +599,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -623,7 +623,7 @@ Suprimir apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -907,7 +907,7 @@ Punts de referència apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -915,7 +915,7 @@ Divises apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -935,7 +935,7 @@ ETFs sense País apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -943,7 +943,7 @@ ETFs sense Sector apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -951,7 +951,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -987,7 +987,7 @@ Nombre d’Activitats apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -995,7 +995,7 @@ Dades Històriques apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -1007,7 +1007,7 @@ Nombre de Sectors apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -1031,7 +1031,7 @@ Nombre de Països apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -1039,7 +1039,7 @@ Recopilar Dades del Perfil apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1919,7 +1919,7 @@ Por apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1935,7 +1935,7 @@ Cobdícia apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2969,10 +2969,6 @@ Okay D’acord - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -3454,6 +3450,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duration @@ -4069,7 +4073,7 @@ How does Ghostfolio work? - Com ho fa Ghostfolio treballar? + Com ho fa Ghostfolio treballar? apps/client/src/app/pages/landing/landing-page.html 286 @@ -4784,7 +4788,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4804,11 +4808,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -4828,7 +4832,7 @@ Mensualment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -4836,7 +4840,7 @@ Anualment apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -5933,7 +5937,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -6285,7 +6289,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7273,7 +7281,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7695,7 +7703,7 @@ AI prompt has been copied to the clipboard apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Gather Recent Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Gather All Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - new - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investment diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 313a22ce3..eab9ec2cb 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -238,7 +238,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -262,7 +262,7 @@ Löschen apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -526,7 +526,7 @@ Historische Daten apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -570,7 +570,7 @@ Letzte historische Marktdaten synchronisieren apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -578,7 +578,7 @@ Alle historischen Marktdaten synchronisieren apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -586,7 +586,7 @@ Profildaten synchronisieren apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1188,10 +1188,6 @@ Okay Okay - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -1741,6 +1737,14 @@ 32 + + popular + beliebt + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Dauer @@ -2506,7 +2510,7 @@ {VAR_PLURAL, plural, =1 {Profil} other {Profile}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -2686,7 +2690,7 @@ Monatlich apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -2738,7 +2742,7 @@ Anzahl Länder apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -2746,7 +2750,7 @@ Anzahl Sektoren apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -2770,7 +2774,7 @@ Angst apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -2786,7 +2790,7 @@ Gier apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2802,7 +2806,7 @@ Filtern nach... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -3342,7 +3346,7 @@ Anzahl Aktivitäten apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -3402,7 +3406,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3538,7 +3542,7 @@ Jährlich apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3606,7 +3610,7 @@ Keine Aktivitäten apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4822,7 +4826,7 @@ ETFs ohne Länder apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4830,7 +4834,7 @@ ETFs ohne Sektoren apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -5066,7 +5070,7 @@ Währungen apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6248,11 +6252,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6709,7 +6713,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7067,6 +7071,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7297,7 +7305,7 @@ löschen apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7719,7 +7727,7 @@ KI-Anweisung wurde in die Zwischenablage kopiert apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7799,7 +7807,7 @@ Öffne Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8358,14 +8366,6 @@ 224 - - new - neu - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investition diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index e99def710..d3439f964 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -239,7 +239,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -263,7 +263,7 @@ Eliminar apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -527,7 +527,7 @@ Datos históricos apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -571,7 +571,7 @@ Recopilar datos del perfil apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1173,10 +1173,6 @@ Okay De acuerdo - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -1726,6 +1722,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duración @@ -2491,7 +2495,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -2715,7 +2719,7 @@ Mensual apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -2723,7 +2727,7 @@ Número de sectores apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -2747,7 +2751,7 @@ Número de países apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -2755,7 +2759,7 @@ Miedo apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -2771,7 +2775,7 @@ Codicia apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2787,7 +2791,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -3327,7 +3331,7 @@ Número de operaciones apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -3379,7 +3383,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3523,7 +3527,7 @@ Anual apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3591,7 +3595,7 @@ Sin operaciones apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4799,7 +4803,7 @@ ETFs sin países apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4807,7 +4811,7 @@ ETFs sin sectores apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -5043,7 +5047,7 @@ Divisas apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6225,11 +6229,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6686,7 +6690,7 @@ Índices de referencia apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7044,6 +7048,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7274,7 +7282,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7696,7 +7704,7 @@ El prompt para la IA ha sido copiado al portapapeles apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7776,7 +7784,7 @@ Abrir Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8013,7 +8021,7 @@ Recopilar datos históricos recientes del mercado apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8021,7 +8029,7 @@ Recopilar todos los datos históricos del mercado apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8359,14 +8367,6 @@ 224 - - new - nuevo - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Inversión diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 119ba3d02..41f6371f2 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -302,7 +302,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -326,7 +326,7 @@ Supprimer apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -570,7 +570,7 @@ Filtrer par... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -606,7 +606,7 @@ Nombre d’Activités apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -614,7 +614,7 @@ Données Historiques apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -626,7 +626,7 @@ Nombre de Secteurs apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -650,7 +650,7 @@ Nombre de Pays apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -658,7 +658,7 @@ Obtenir les Données du Profil apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1078,7 +1078,7 @@ Peur apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1094,7 +1094,7 @@ Avidité apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -1484,10 +1484,6 @@ Okay D’accord - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -2061,6 +2057,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Durée @@ -2614,7 +2618,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -2642,7 +2646,7 @@ Mensuel apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -3046,7 +3050,7 @@ {VAR_PLURAL, plural, =1 {Profil} other {Profils}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -3522,7 +3526,7 @@ Annuel apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3590,7 +3594,7 @@ Aucune Activité apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4798,7 +4802,7 @@ ETF sans Pays apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4806,7 +4810,7 @@ ETF sans Secteurs apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -5042,7 +5046,7 @@ Devises apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6224,11 +6228,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6685,7 +6689,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7273,7 +7281,7 @@ Supprimer apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7695,7 +7703,7 @@ Le prompt IA a été copié dans le presse-papiers apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Ouvrir Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Collecter les données récentes du marché historique apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Collecter toutes les données du marché historique apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - nouveau - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investissement diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 66c141baa..d3eb68c71 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -239,7 +239,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -263,7 +263,7 @@ Elimina apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -527,7 +527,7 @@ Dati storici apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -571,7 +571,7 @@ Raccogli i dati del profilo apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1173,10 +1173,6 @@ Okay Bene - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -1726,6 +1722,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duration @@ -2491,7 +2495,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -2715,7 +2719,7 @@ Mensile apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -2723,7 +2727,7 @@ Numero di settori apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -2747,7 +2751,7 @@ Numero di paesi apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -2755,7 +2759,7 @@ Paura apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -2771,7 +2775,7 @@ Avidità apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2787,7 +2791,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -3327,7 +3331,7 @@ Conteggio attività apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -3379,7 +3383,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3523,7 +3527,7 @@ Annuale apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3591,7 +3595,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4799,7 +4803,7 @@ ETF senza paesi apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4807,7 +4811,7 @@ ETF senza settori apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -5043,7 +5047,7 @@ Valute apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6225,11 +6229,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6686,7 +6690,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7044,6 +7048,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7274,7 +7282,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7696,7 +7704,7 @@ L’AI prompt è stato copiato negli appunti apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7776,7 +7784,7 @@ Apri Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8013,7 +8021,7 @@ Raccogli dati storici di mercato recenti apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8021,7 +8029,7 @@ Raccogli tutti i dati storici di mercato apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8359,14 +8367,6 @@ 224 - - new - nuovo - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investimento diff --git a/apps/client/src/locales/messages.ja.xlf b/apps/client/src/locales/messages.ja.xlf index 0e72d5649..2a4ef7b7b 100644 --- a/apps/client/src/locales/messages.ja.xlf +++ b/apps/client/src/locales/messages.ja.xlf @@ -532,7 +532,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -556,7 +556,7 @@ 削除 apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -796,7 +796,7 @@ 通貨 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -816,7 +816,7 @@ 国に縛られないETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -824,7 +824,7 @@ セクター別ではないETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -844,7 +844,7 @@ 絞り込み... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -880,7 +880,7 @@ アクティビティ数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -888,7 +888,7 @@ 過去データ apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -900,7 +900,7 @@ セクター数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -924,7 +924,7 @@ 国の数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -932,7 +932,7 @@ 直近の市場実績データを収集する apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -940,7 +940,7 @@ すべての過去の市場データを収集する apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -948,7 +948,7 @@ プロファイルデータを収集する apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1664,7 +1664,7 @@ 恐怖 apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1680,7 +1680,7 @@ Greed apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2694,10 +2694,6 @@ Okay オッケー - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -3151,6 +3147,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration 持続時間 @@ -4440,7 +4444,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4468,7 +4472,7 @@ 毎月 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -4476,7 +4480,7 @@ 年次 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -5413,7 +5417,7 @@ {VAR_PLURAL, plural, =1 {プロフィール} other {プロフィール}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -5741,7 +5745,7 @@ アクティビティなし apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -6273,11 +6277,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6710,7 +6714,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7060,6 +7064,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7298,7 +7306,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7720,7 +7728,7 @@ AI prompt has been copied to the clipboard apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7800,7 +7808,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8359,14 +8367,6 @@ 224 - - new - new - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investment diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index b7d7cad22..ea2df8596 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -433,7 +433,7 @@ Copy - Copy + 복사 libs/ui/src/lib/notifications/alert-dialog/alert-dialog.html 20 @@ -532,7 +532,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -556,7 +556,7 @@ 삭제 apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -605,7 +605,7 @@ Paid - Paid + 유료 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts 122 @@ -693,7 +693,7 @@ and is driven by the efforts of its contributors - 기여자들의 노력으로 발전하고 있습니다 + 기여자들의 노력으로 발전하고 있습니다 apps/client/src/app/pages/about/overview/about-overview-page.html 50 @@ -769,7 +769,7 @@ Watch the Ghostfol.io Trailer on YouTube - Watch the Ghostfol.io Trailer on YouTube + YouTube에서 Ghostfol.io 트레일러 보기 apps/client/src/app/pages/landing/landing-page.html 19 @@ -796,7 +796,7 @@ 통화 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -805,7 +805,7 @@ Category - Category + 카테고리 apps/client/src/app/pages/resources/personal-finance-tools/product-page.html 77 @@ -816,7 +816,7 @@ 국가 정보 없는 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -824,7 +824,7 @@ 섹터 정보 없는 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -844,7 +844,7 @@ 다음 기준으로 필터... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -869,7 +869,7 @@ Data Gathering Frequency - Data Gathering Frequency + 데이터 수집 빈도 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 454 @@ -880,7 +880,7 @@ 거래 건수 apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -888,7 +888,7 @@ 과거 데이터 apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -900,7 +900,7 @@ 섹터 수 apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -913,7 +913,7 @@ Subscription History - Subscription History + 구독 내역 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 136 @@ -924,7 +924,7 @@ 국가 수 apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -932,7 +932,7 @@ 최근 과거 시장 데이터 수집 apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -940,7 +940,7 @@ 전체 과거 시장 데이터 수집 apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -948,7 +948,7 @@ 프로필 데이터 수집 apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1097,7 +1097,7 @@ Total - Total + 합계 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 155 @@ -1664,7 +1664,7 @@ 두려움 apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1680,7 +1680,7 @@ 탐욕 apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -1721,7 +1721,7 @@ The source code is fully available as open source software (OSS) under the AGPL-3.0 license - 소스 코드는 오픈 소스 소프트웨어로 완전히 공개되어 있으며, AGPL-3.0 라이선스 하에 제공됩니다 + 소스 코드는 오픈 소스 소프트웨어로 완전히 공개되어 있으며, AGPL-3.0 라이선스 하에 제공됩니다 apps/client/src/app/pages/about/overview/about-overview-page.html 16 @@ -2293,7 +2293,7 @@ Ghostfolio in Numbers: Monthly Active Users (MAU) - Ghostfolio in Numbers: Monthly Active Users (MAU) + Ghostfolio 통계: 월간 활성 사용자 수(MAU) apps/client/src/app/pages/landing/landing-page.html 63 @@ -2333,7 +2333,7 @@ The value has been copied to the clipboard - The value has been copied to the clipboard + 값이 클립보드에 복사되었습니다. libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 @@ -2493,7 +2493,7 @@ Coupon - Coupon + 쿠폰 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts 127 @@ -2529,7 +2529,7 @@ Close Account - Close Account + 계정 폐쇄 apps/client/src/app/components/user-account-settings/user-account-settings.html 337 @@ -2549,7 +2549,7 @@ Contributors to Ghostfolio - Contributors to Ghostfolio + Ghostfolio의 기여자 apps/client/src/app/pages/about/overview/about-overview-page.html 54 @@ -2694,10 +2694,6 @@ Okay 좋아요 - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -2889,7 +2885,7 @@ Financial Planning - Financial Planning + 재무 설계 libs/ui/src/lib/i18n.ts 108 @@ -3151,6 +3147,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration 기간 @@ -3217,7 +3221,7 @@ For security reasons, please delete all activities and accounts first before your Ghostfolio account can be closed. - For security reasons, please delete all activities and accounts first before your Ghostfolio account can be closed. + 보안을 위해 Ghostfolio 계정을 폐쇄하려면 먼저 모든 거래 내역과 계좌를 삭제해 주세요. apps/client/src/app/components/user-account-settings/user-account-settings.html 348 @@ -3345,7 +3349,7 @@ Creation - Creation + 생성 apps/client/src/app/components/admin-overview/admin-overview.html 185 @@ -3385,7 +3389,7 @@ just now - just now + 방금 전 apps/client/src/app/components/admin-users/admin-users.component.ts 217 @@ -3709,7 +3713,7 @@ Oops! Could not delete the asset profiles. - Oops! Could not delete the asset profiles. + 이런! 자산 프로필을 삭제할 수 없습니다. apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 52 @@ -3797,7 +3801,7 @@ At Ghostfolio, transparency is at the core of our values. We publish the source code as open source software (OSS) under the AGPL-3.0 license and we openly share aggregated key metrics of the platform’s operational status. - Ghostfolio는 투명성을 핵심 가치로 삼습니다. 우리는 소스 코드를 오픈 소스 소프트웨어로 공개하며, AGPL-3.0 라이선스 하에 배포합니다. 또한 플랫폼 운영 현황에 대한 집계된 핵심 지표를 공개적으로 공유합니다. + Ghostfolio는 투명성을 핵심 가치로 삼습니다. 우리는 소스 코드를 오픈 소스 소프트웨어로 공개하며, AGPL-3.0 라이선스 하에 배포합니다. 또한 플랫폼 운영 현황에 대한 집계된 핵심 지표를 공개적으로 공유합니다. apps/client/src/app/pages/open/open-page.html 7 @@ -3893,7 +3897,7 @@ Available on - Available on + 이용 가능 플랫폼 apps/client/src/app/pages/resources/personal-finance-tools/product-page.html 130 @@ -4201,7 +4205,7 @@ Price - Price + 가격 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 171 @@ -4301,7 +4305,7 @@ Trial - Trial + 체험판 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts 126 @@ -4432,7 +4436,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4460,7 +4464,7 @@ 월간 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -4468,7 +4472,7 @@ 매년 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -4609,7 +4613,7 @@ Hourly - Hourly + 매시간 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 214 @@ -4721,7 +4725,7 @@ Expiration - Expiration + 만료 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 204 @@ -5030,7 +5034,7 @@ Upgrade to Ghostfolio Premium - Upgrade to Ghostfolio Premium + Ghostfolio 프리미엄으로 업그레이드 libs/ui/src/lib/premium-indicator/premium-indicator.component.html 4 @@ -5118,7 +5122,7 @@ Stock Tracking - Stock Tracking + 주식 추적 libs/ui/src/lib/i18n.ts 111 @@ -5142,7 +5146,7 @@ Web - Web + libs/ui/src/lib/i18n.ts 120 @@ -5338,7 +5342,7 @@ Portfolio Filters - Portfolio Filters + 포트폴리오 필터 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 63 @@ -5402,10 +5406,10 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} - {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} + {VAR_PLURAL, plural, =1 {프로필} other {프로필}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -5714,7 +5718,7 @@ Ghostfolio in Numbers: Pulls on Docker Hub - Ghostfolio in Numbers: Pulls on Docker Hub + Ghostfolio 통계: Docker Hub 다운로드 수 apps/client/src/app/pages/landing/landing-page.html 101 @@ -5733,7 +5737,7 @@ 거래 내역 없음 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -6273,11 +6277,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6346,7 +6350,7 @@ Expires () - Expires () + 만료 () apps/client/src/app/components/admin-users/admin-users.html 34 @@ -6507,7 +6511,7 @@ Daily - Daily + 매일 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 210 @@ -6710,7 +6714,7 @@ 벤치마크 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -6807,7 +6811,7 @@ Oops! Could not delete the asset profile. - Oops! Could not delete the asset profile. + 이런! 자산 프로필을 삭제할 수 없습니다. apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 51 @@ -6927,7 +6931,7 @@ Do you really want to delete these asset profiles? - Do you really want to delete these asset profiles? + 개의 자산 프로필을 정말 삭제하시겠습니까? apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 67 @@ -7060,6 +7064,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7151,7 +7159,7 @@ Performance with currency effect Performance - 환율 효과 반영 수익률 수익률 + 환율 효과 반영 수익률 수익률 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 83 @@ -7167,7 +7175,7 @@ Tax Reporting - Tax Reporting + 세금 보고 libs/ui/src/lib/i18n.ts 112 @@ -7175,7 +7183,7 @@ Change with currency effect Change - 환율 효과 반영 변동 변동 + 환율 효과 반영 변동 변동 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 63 @@ -7191,7 +7199,7 @@ Compare Ghostfolio to - - Compare Ghostfolio to - + Ghostfolio와 비교 - apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html 32 @@ -7231,7 +7239,7 @@ Dividend Tracking - Dividend Tracking + 배당 추적 libs/ui/src/lib/i18n.ts 105 @@ -7295,10 +7303,10 @@ Delete - Delete + 삭제 apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7411,7 +7419,7 @@ Investment Research - Investment Research + 투자 리서치 libs/ui/src/lib/i18n.ts 109 @@ -7693,7 +7701,7 @@ Net Worth Tracking - Net Worth Tracking + 순자산 추적 libs/ui/src/lib/i18n.ts 110 @@ -7720,7 +7728,7 @@ AI 프롬프트가 클립보드에 복사되었습니다. apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7800,7 +7808,7 @@ 오픈 Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -7825,7 +7833,7 @@ Ghostfolio in Numbers: Stars on GitHub - Ghostfolio in Numbers: Stars on GitHub + Ghostfolio 통계: GitHub 스타 수 apps/client/src/app/pages/landing/landing-page.html 82 @@ -8131,7 +8139,7 @@ ETF Tracking - ETF Tracking + ETF 추적 libs/ui/src/lib/i18n.ts 106 @@ -8297,7 +8305,7 @@ Post to Ghostfolio on X (formerly Twitter) - Post to Ghostfolio on X (formerly Twitter) + X(이전의 Twitter)에서 Ghostfolio에 게시하세요. apps/client/src/app/pages/about/overview/about-overview-page.html 85 @@ -8359,14 +8367,6 @@ 224 - - new - 새로운 - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment 투자 @@ -8526,7 +8526,7 @@ If you encounter a bug, would like to suggest an improvement or a new feature, please join the Ghostfolio Slack community, post to @ghostfolio_ - 버그가 발생하거나 개선 사항이나 새로운 기능을 제안하고 싶다면 Ghostfolio 슬랙 커뮤니티에 가입하고 @ghostfolio_에 게시하세요. + 버그가 발생하거나 개선 사항이나 새로운 기능을 제안하고 싶다면 Ghostfolio 슬랙 커뮤니티에 가입하고 @ghostfolio_에 게시하세요. apps/client/src/app/pages/about/overview/about-overview-page.html 71 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 0f02c4747..1654368f8 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -238,7 +238,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -262,7 +262,7 @@ Verwijderen apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -451,7 +451,7 @@ Find an account... - Find an account... + Zoek een account... libs/ui/src/lib/assistant/assistant.component.ts 447 @@ -526,7 +526,7 @@ Historische gegevens apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -570,7 +570,7 @@ Verzamel profielgegevens apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1172,10 +1172,6 @@ Okay Oké - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -1419,7 +1415,7 @@ The value has been copied to the clipboard - The value has been copied to the clipboard + De waarde is naar het klembord gekopieerd libs/ui/src/lib/notifications/alert-dialog/alert-dialog.component.ts 46 @@ -1503,7 +1499,7 @@ Copy - Copy + Kopiëren libs/ui/src/lib/notifications/alert-dialog/alert-dialog.html 20 @@ -1725,6 +1721,14 @@ 32 + + popular + populair + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Looptijd @@ -2490,7 +2494,7 @@ {VAR_PLURAL, plural, =1 {Profiel} other {Profielen}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -2714,7 +2718,7 @@ Maandelijks apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -2722,7 +2726,7 @@ Aantal sectoren apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -2746,7 +2750,7 @@ Aantal landen apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -2754,7 +2758,7 @@ Angst apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -2770,7 +2774,7 @@ Hebzucht apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2786,7 +2790,7 @@ Filter op... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -2879,7 +2883,7 @@ Close Account - Close Account + Account Sluiten apps/client/src/app/components/user-account-settings/user-account-settings.html 337 @@ -3326,7 +3330,7 @@ Aantal activiteiten apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -3378,7 +3382,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3522,7 +3526,7 @@ Jaarlijks apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3590,7 +3594,7 @@ Geen activiteiten apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4399,7 +4403,7 @@ For security reasons, please delete all activities and accounts first before your Ghostfolio account can be closed. - For security reasons, please delete all activities and accounts first before your Ghostfolio account can be closed. + Verwijder om veiligheidsredenen eerst alle activiteiten en rekeningen voordat uw Ghostfolio account kan worden gesloten. apps/client/src/app/components/user-account-settings/user-account-settings.html 348 @@ -4798,7 +4802,7 @@ ETF’s zonder Landen apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4806,7 +4810,7 @@ ETF’s zonder Sectoren apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -4859,7 +4863,7 @@ The source code is fully available as open source software (OSS) under the AGPL-3.0 license - De broncode is volledig beschikbaar als open source software (OSS) onder de AGPL-3.0-licentie + De broncode is volledig beschikbaar als open source software (OSS) onder de AGPL-3.0-licentie apps/client/src/app/pages/about/overview/about-overview-page.html 16 @@ -4947,7 +4951,7 @@ At Ghostfolio, transparency is at the core of our values. We publish the source code as open source software (OSS) under the AGPL-3.0 license and we openly share aggregated key metrics of the platform’s operational status. - Bij Ghostfolio is transparantie één van onze kernwaarden. We publiceren de broncode als open source software (OSS) onder de AGPL-3.0 licentie en we delen openlijk geaggregeerde kerncijfers van de operationele status van het platform. + Bij Ghostfolio is transparantie één van onze kernwaarden. We publiceren de broncode als open source software (OSS) onder de AGPL-3.0 licentie en we delen openlijk geaggregeerde kerncijfers van de operationele status van het platform. apps/client/src/app/pages/open/open-page.html 7 @@ -5042,7 +5046,7 @@ Valuta apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6224,11 +6228,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6685,7 +6689,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7086,7 +7094,7 @@ Tax Reporting - Tax Reporting + Belastingrapportage libs/ui/src/lib/i18n.ts 112 @@ -7210,7 +7218,7 @@ is Open Source Software - is Open Source Software + is open source software apps/client/src/app/pages/resources/personal-finance-tools/product-page.html 183 @@ -7250,7 +7258,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year - to use our referral link and get a Ghostfolio Premium membership for one year + om onze referral link te gebruiken en een jaar lang Ghostfolio Premium-lidmaatschap te krijgen apps/client/src/app/pages/pricing/pricing-page.html 340 @@ -7273,7 +7281,7 @@ Verwijder apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7668,7 +7676,7 @@ Net Worth Tracking - Net Worth Tracking + Netto waarde volgen libs/ui/src/lib/i18n.ts 110 @@ -7695,7 +7703,7 @@ AI-prompt is naar het klembord gekopieerd apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Verzamel Recente Marktgegevens apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Verzamel Alle Marktgegevens apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8288,7 +8296,7 @@ Post to Ghostfolio on X (formerly Twitter) - Post to Ghostfolio on X (formerly Twitter) + Post naar Ghostfolio op X (voorheen Twitter) apps/client/src/app/pages/about/overview/about-overview-page.html 85 @@ -8358,14 +8366,6 @@ 224 - - new - nieuw - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investering @@ -8533,7 +8533,7 @@ If you encounter a bug, would like to suggest an improvement or a new feature, please join the Ghostfolio Slack community, post to @ghostfolio_ - Als je een bug tegenkomt, een verbetering wilt voorstellen of een nieuwe functie wilt toevoegen, word dan lid van de Ghostfolio Slack-community. Stuur een bericht naar @ghostfolio_ + Als je een bug tegenkomt, een verbetering wilt voorstellen of een nieuwe functie wilt toevoegen, word dan lid van de Ghostfolio Slack-community. Stuur een bericht naar @ghostfolio_ apps/client/src/app/pages/about/overview/about-overview-page.html 71 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index 2c993890c..60b2f4115 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -523,7 +523,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -547,7 +547,7 @@ Usuń apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -787,7 +787,7 @@ Waluty apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -807,7 +807,7 @@ ETF-y bez Krajów apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -815,7 +815,7 @@ ETF-y bez Sektorów apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -835,7 +835,7 @@ Filtruj według... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -871,7 +871,7 @@ Liczba Aktywności apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -879,7 +879,7 @@ Dane Historyczne apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -891,7 +891,7 @@ Liczba Sektorów apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -915,7 +915,7 @@ Liczba Krajów apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -923,7 +923,7 @@ Zbierz Dane Profilu apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1631,7 +1631,7 @@ Zagrożenie apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1647,7 +1647,7 @@ Zachłanność apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2661,10 +2661,6 @@ Okay Okej - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -3118,6 +3114,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duration @@ -4399,7 +4403,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4427,7 +4431,7 @@ Miesięcznie apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -4435,7 +4439,7 @@ Rocznie apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -5328,7 +5332,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -5656,7 +5660,7 @@ Brak transakcji apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -6224,11 +6228,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6685,7 +6689,7 @@ Punkty Odniesienia apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7273,7 +7281,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7695,7 +7703,7 @@ Prompt AI został skopiowany do schowka apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Otwórz Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Zbierz najnowsze historyczne dane rynkowe apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Zbierz wszystkie historyczne dane rynkowe apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - nowy - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Inwestycja diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index af9f30ed2..56f2d5ab4 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -302,7 +302,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -326,7 +326,7 @@ Eliminar apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -570,7 +570,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -598,7 +598,7 @@ Dados Históricos apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -610,7 +610,7 @@ Contagem de Países apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -618,7 +618,7 @@ Contagem de Setores apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -642,7 +642,7 @@ Recolher Dados de Perfíl apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -930,7 +930,7 @@ Medo apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -946,7 +946,7 @@ Ganância apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -1472,10 +1472,6 @@ Okay OK - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -2037,6 +2033,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duration @@ -2550,7 +2554,7 @@ Mensalmente apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -2938,7 +2942,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -3338,7 +3342,7 @@ Nº de Atividades apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -3454,7 +3458,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3522,7 +3526,7 @@ Anualmente apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -3590,7 +3594,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -4798,7 +4802,7 @@ ETFs sem países apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -4806,7 +4810,7 @@ ETFs sem setores apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -5042,7 +5046,7 @@ Moedas apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -6224,11 +6228,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6685,7 +6689,7 @@ Referências apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7273,7 +7281,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7695,7 +7703,7 @@ AI prompt has been copied to the clipboard apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Gather Recent Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Gather All Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - new - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Investment diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 8514610e5..1acc6ea9e 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -483,7 +483,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -507,7 +507,7 @@ Sil apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -751,7 +751,7 @@ Para Birimleri apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -771,7 +771,7 @@ Ülkesi Olmayan ETF’ler apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -779,7 +779,7 @@ Sektörü Olmayan ETF’ler apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -787,7 +787,7 @@ Filtrele... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -823,7 +823,7 @@ İşlem Sayısı apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -831,7 +831,7 @@ Tarihsel Veri apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -843,7 +843,7 @@ Sektör Sayısı apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -867,7 +867,7 @@ Ülke Sayısı apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -875,7 +875,7 @@ Profil Verisini Getir apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1479,7 +1479,7 @@ Korku apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1495,7 +1495,7 @@ Açgözlülük apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2096,7 +2096,7 @@ 1Y - 1Y + 1Y apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 232 @@ -2185,10 +2185,6 @@ Okay Tamam - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -2610,6 +2606,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Duration @@ -3803,7 +3807,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -3831,7 +3835,7 @@ Aylık apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -3839,7 +3843,7 @@ Yıllık apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -4269,7 +4273,7 @@ This overview page features a curated collection of personal finance tools compared to the open source alternative Ghostfolio. If you value transparency, data privacy, and community collaboration, Ghostfolio provides an excellent opportunity to take control of your financial management. - Bu genel bakış sayfası, diğer kişisel finans araçlarının seçilmiş bir koleksiyonunun açık kaynak alternatifiGhostfolio ile karşılaştırmasını sunmaktadır.. Şeffaflığa, veri gizliliğine ve topluluk işbirliğine değer veriyorsanız Ghostfolio, finansal yönetiminizin kontrolünü ele almak için mükemmel Şeffaflığa, veri gizliliğine ve topluluk işbirliğine değer veriyorsanız Ghostfolio, finansal yönetiminizin kontrolünü ele almak için mükemmel bir fırsat sunuyor. + Bu genel bakış sayfası, diğer kişisel finans araçlarının seçilmiş bir koleksiyonunun açık kaynak alternatifiGhostfolio ile karşılaştırmasını sunmaktadır.. Şeffaflığa, veri gizliliğine ve topluluk işbirliğine değer veriyorsanız Ghostfolio, finansal yönetiminizin kontrolünü ele almak için mükemmel bir fırsat sunuyor. apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html 9 @@ -4996,7 +5000,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -5324,7 +5328,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -6224,11 +6228,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6685,7 +6689,7 @@ Kıyaslamalar apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7043,6 +7047,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7273,7 +7281,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7695,7 +7703,7 @@ Yapay zeka istemi panoya kopyalandı apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Duck.ai’yi aç apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Yakın Geçmiş Piyasa Verilerini Topla apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Tüm Geçmiş Piyasa Verilerini Topla apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - yeni - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Yatırım diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 8152c9936..1a8a72690 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -623,7 +623,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -647,7 +647,7 @@ Видалити apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -879,7 +879,7 @@ Порівняльні показники apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -887,7 +887,7 @@ Валюти apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -907,7 +907,7 @@ ETF без країн apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -915,7 +915,7 @@ ETF без секторів apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -923,7 +923,7 @@ Фільтрувати за... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -983,7 +983,7 @@ Кількість активностей apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -991,7 +991,7 @@ Історичні дані apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -1003,7 +1003,7 @@ Кількість секторів apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -1027,7 +1027,7 @@ Кількість країн apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -1035,7 +1035,7 @@ Зібрати дані профілю apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -2063,7 +2063,7 @@ Страх apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -2079,7 +2079,7 @@ Жадібність apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2857,10 +2857,6 @@ Okay ОК - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -3750,6 +3746,14 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration Тривалість @@ -5124,7 +5128,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -5144,11 +5148,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -5168,7 +5172,7 @@ Щомісячно apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -5176,7 +5180,7 @@ Щорічно apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -6219,7 +6223,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -6739,7 +6743,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -7085,6 +7089,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7227,7 +7235,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -7703,7 +7711,7 @@ Запит AI скопійовано в буфер обміну apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7775,7 +7783,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8012,7 +8020,7 @@ Gather Recent Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8020,7 +8028,7 @@ Gather All Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8358,14 +8366,6 @@ 224 - - new - новий - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment Інвестиція diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index bf7304782..67d52db96 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -501,7 +501,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -524,7 +524,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -744,7 +744,7 @@ Currencies apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -762,14 +762,14 @@ ETFs without Countries apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 ETFs without Sectors apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -787,7 +787,7 @@ Filter by... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -820,14 +820,14 @@ Activities Count apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 Historical Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -838,7 +838,7 @@ Sectors Count apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -859,28 +859,28 @@ Countries Count apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 Gather Recent Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 Gather All Historical Market Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 Gather Profile Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1527,7 +1527,7 @@ Fear apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1542,7 +1542,7 @@ Greed apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -2460,10 +2460,6 @@ Okay - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -2891,6 +2887,13 @@ 32 + + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration @@ -4051,7 +4054,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4076,14 +4079,14 @@ Monthly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 Yearly apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -4926,7 +4929,7 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 @@ -5226,7 +5229,7 @@ No Activities apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -5704,11 +5707,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6093,7 +6096,7 @@ Benchmarks apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -6416,6 +6419,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -6628,7 +6635,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7009,7 +7016,7 @@ AI prompt has been copied to the clipboard apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7079,7 +7086,7 @@ Open Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -7579,13 +7586,6 @@ 224 - - new - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 2230a6f86..025e5bb0e 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -532,7 +532,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 278 + 284 apps/client/src/app/components/admin-platform/admin-platform.component.html @@ -556,7 +556,7 @@ 删除 apps/client/src/app/components/admin-market-data/admin-market-data.html - 301 + 307 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -796,7 +796,7 @@ 货币 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 135 + 132 apps/client/src/app/pages/public/public-page.html @@ -816,7 +816,7 @@ 没有国家的 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 140 + 137 @@ -824,7 +824,7 @@ 无行业类别的 ETF apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 145 + 142 @@ -844,7 +844,7 @@ 过滤... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 374 + 366 @@ -880,7 +880,7 @@ 活动计数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 156 + 162 @@ -888,7 +888,7 @@ 历史数据 apps/client/src/app/components/admin-market-data/admin-market-data.html - 165 + 171 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -900,7 +900,7 @@ 行业数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 174 + 180 @@ -924,7 +924,7 @@ 国家数 apps/client/src/app/components/admin-market-data/admin-market-data.html - 183 + 189 @@ -932,7 +932,7 @@ 收集个人资料数据 apps/client/src/app/components/admin-market-data/admin-market-data.html - 234 + 240 apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -1341,7 +1341,7 @@ Explore - Explore + 探索 apps/client/src/app/pages/resources/overview/resources-overview.component.html 11 @@ -1640,7 +1640,7 @@ 恐惧 apps/client/src/app/components/home-market/home-market.component.ts - 46 + 48 apps/client/src/app/components/markets/markets.component.ts @@ -1656,7 +1656,7 @@ 贪婪 apps/client/src/app/components/home-market/home-market.component.ts - 47 + 49 apps/client/src/app/components/markets/markets.component.ts @@ -1789,7 +1789,7 @@ Code - Code + 代码 apps/client/src/app/components/admin-overview/admin-overview.html 159 @@ -2670,10 +2670,6 @@ Okay 好的 - - apps/client/src/app/components/user-account-membership/user-account-membership.component.ts - 150 - apps/client/src/app/core/http-response.interceptor.ts 86 @@ -3127,9 +3123,17 @@ 32 + + popular + popular + + apps/client/src/app/components/admin-settings/admin-settings.component.html + 79 + + Duration - Duration + 持续时间 apps/client/src/app/components/admin-overview/admin-overview.html 172 @@ -4416,7 +4420,7 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 78 + 80 libs/ui/src/lib/i18n.ts @@ -4444,7 +4448,7 @@ 每月 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 92 + 94 @@ -4452,7 +4456,7 @@ 每年 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 93 + 95 @@ -5389,12 +5393,12 @@ {VAR_PLURAL, plural, =1 {Profile} other {Profiles}} apps/client/src/app/components/admin-market-data/admin-market-data.html - 249 + 255 Loan - Loan + 贷款 libs/ui/src/lib/i18n.ts 64 @@ -5714,10 +5718,10 @@ No Activities - No Activities + 暂无活动 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 150 + 147 @@ -6249,11 +6253,11 @@ apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 82 + 84 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 98 + 100 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6686,7 +6690,7 @@ 基准 apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 130 + 127 @@ -7044,6 +7048,10 @@ apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 80 + + apps/client/src/app/components/user-account-membership/user-account-membership.component.ts + 150 + apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 239 @@ -7167,7 +7175,7 @@ has been copied to the clipboard - has been copied to the clipboard + 已复制到剪贴板 apps/client/src/app/components/admin-overview/admin-overview.component.ts 388 @@ -7274,7 +7282,7 @@ Delete apps/client/src/app/components/admin-market-data/admin-market-data.html - 244 + 250 @@ -7696,7 +7704,7 @@ AI 提示已复制到剪贴板 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 211 + 217 @@ -7776,7 +7784,7 @@ 打开 Duck.ai apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts - 212 + 218 @@ -8013,7 +8021,7 @@ 收集近期历史市场数据 apps/client/src/app/components/admin-market-data/admin-market-data.html - 225 + 231 @@ -8021,7 +8029,7 @@ 收集所有历史市场数据 apps/client/src/app/components/admin-market-data/admin-market-data.html - 230 + 236 @@ -8359,14 +8367,6 @@ 224 - - new - 新增 - - apps/client/src/app/components/admin-settings/admin-settings.component.html - 79 - - Investment 投资 diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 045de2eb6..653efe376 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -376,12 +376,16 @@ ngx-skeleton-loader { visibility: hidden; } +.lead { + font-weight: unset; +} + .line-height-1 { line-height: 1; } -.lead { - font-weight: unset; +.line-height-normal { + line-height: normal; } .mat-mdc-button-base { diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index c60170bbb..633e13f24 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -11,7 +11,6 @@ export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = DataSource.MANUAL; -export const ghostfolioFearAndGreedIndexDataSourceStocks = DataSource.RAPID_API; export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`; export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`; @@ -330,4 +329,9 @@ 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_DEFAULT_LIMIT = 10; +export const THROTTLE_DEFAULT_TTL = ms('1 minute'); +export const THROTTLE_SIGNUP_LIMIT = 5; +export const THROTTLE_SIGNUP_TTL = ms('1 hour'); + export const UNKNOWN_KEY = 'UNKNOWN'; diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts index 28b3f8495..b3aae6a02 100644 --- a/libs/common/src/lib/personal-finance-tools.ts +++ b/libs/common/src/lib/personal-finance-tools.ts @@ -14,6 +14,13 @@ export const personalFinanceTools: Product[] = [ slogan: 'Lifetime Personal Finance Control, One Single Payment.', url: 'https://www.mechcad.net' }, + { + categories: ['WEALTH_MANAGEMENT'], + key: 'akkuro', + name: 'Akkuro', + slogan: 'Composable Banking', + url: 'https://akkuro.com' + }, { categories: ['ETF_TRACKING', 'STOCK_TRACKING'], founded: 2023, @@ -215,7 +222,7 @@ export const personalFinanceTools: Product[] = [ name: 'Buxfer', origin: 'US', platforms: ['WEB'], - pricingPerYear: '$48', + pricingPerYear: '$96', regions: ['Global'], slogan: 'Take control of your financial future', url: 'https://www.buxfer.com' diff --git a/libs/common/src/lib/routes/interfaces/internal-route.interface.ts b/libs/common/src/lib/routes/interfaces/internal-route.interface.ts index f08cf8b5c..8240db46a 100644 --- a/libs/common/src/lib/routes/interfaces/internal-route.interface.ts +++ b/libs/common/src/lib/routes/interfaces/internal-route.interface.ts @@ -5,5 +5,5 @@ export interface InternalRoute { path?: string; routerLink: string[]; subRoutes?: Record; - title: string; + title?: string; } diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index 2785efdde..8132520fc 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -153,8 +153,7 @@ export const internalRoutes = { webauthn: { excludeFromAssistant: true, path: 'webauthn', - routerLink: ['/webauthn'], - title: $localize`Sign in` + routerLink: ['/webauthn'] }, zen: { excludeFromAssistant: true, diff --git a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html index 36179b719..fa30d0c03 100644 --- a/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html +++ b/libs/ui/src/lib/assistant/assistant-list-item/assistant-list-item.html @@ -8,7 +8,10 @@ @if (item && isAsset(item)) {
{{ item?.symbol ?? '' | gfSymbol }} · {{ item?.currency }} + >{{ item?.symbol ?? '' | gfSymbol }} + @if (item.currency) { + · {{ item.currency }} + } @if (item?.assetSubClassString) { · {{ item.assetSubClassString }} } diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.html b/libs/ui/src/lib/page-tabs/page-tabs.component.html index 2898bcc79..34a5a3e26 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.html +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.html @@ -45,9 +45,15 @@ - -
+ + + +
diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.scss b/libs/ui/src/lib/page-tabs/page-tabs.component.scss index ffb3ff33c..246a6147d 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.scss +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.scss @@ -56,6 +56,11 @@ padding: 2rem 0; width: 14rem; + .mat-mdc-tab-list, + .mat-mdc-tab-links { + width: 100%; + } + .mat-mdc-tab-links { flex-direction: column; diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html index 456cd9940..12867662b 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html @@ -25,7 +25,10 @@ } {{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency }} + >{{ lookupItem.symbol | gfSymbol }} + @if (lookupItem.currency) { + · {{ lookupItem.currency }} + } @if (lookupItem.assetSubClass) { · {{ lookupItem.assetSubClassString }} } diff --git a/nx.json b/nx.json index 6b90f437c..1deb022df 100644 --- a/nx.json +++ b/nx.json @@ -67,6 +67,16 @@ } }, "$schema": "./node_modules/nx/schemas/nx-schema.json", + "plugins": [ + { + "plugin": "@nx/webpack/plugin", + "options": { + "buildTargetName": "build", + "serveTargetName": "serve", + "previewTargetName": "preview" + } + } + ], "targetDefaults": { "build": { "dependsOn": ["^build"], diff --git a/package-lock.json b/package-lock.json index f7d55a2fb..4c0b837ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.25.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -29,6 +29,7 @@ "@internationalized/number": "3.6.7", "@ionic/angular": "8.8.12", "@keyv/redis": "5.1.6", + "@nest-lab/throttler-storage-redis": "1.2.0", "@nestjs/bull": "11.0.4", "@nestjs/cache-manager": "3.1.3", "@nestjs/common": "11.1.27", @@ -40,6 +41,7 @@ "@nestjs/platform-express": "11.1.27", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", + "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "2.9.1", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", @@ -70,7 +72,7 @@ "fast-redact": "3.5.0", "fuse.js": "7.3.0", "google-spreadsheet": "3.2.0", - "helmet": "7.0.0", + "helmet": "8.2.0", "http-status-codes": "2.3.0", "ionicons": "8.0.13", "jsonpath": "1.3.0", @@ -164,7 +166,8 @@ "ts-jest": "29.4.0", "ts-node": "10.9.2", "tslib": "2.8.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "webpack-cli": "7.1.0" }, "engines": { "node": ">=22.18.0" @@ -6955,6 +6958,39 @@ "tslib": "^2.4.0" } }, + "node_modules/@nest-lab/throttler-storage-redis": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nest-lab/throttler-storage-redis/-/throttler-storage-redis-1.2.0.tgz", + "integrity": "sha512-tMkUyo68NCKTR+zILk+EC35SMYBtDPZY2mCj7ZaCietWGVTnuP4zwq9ERYfvU6kJv6h8teNZrC6MJCmY6/dljw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/throttler": ">=6.0.0", + "ioredis": ">=5.0.0", + "reflect-metadata": "^0.2.1" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": false + }, + "@nestjs/core": { + "optional": false + }, + "@nestjs/throttler": { + "optional": false + }, + "ioredis": { + "optional": false + }, + "reflect-metadata": { + "optional": false + } + } + }, "node_modules/@nestjs/bull": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz", @@ -7440,6 +7476,17 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, "node_modules/@ngtools/webpack": { "version": "21.2.6", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-21.2.6.tgz", @@ -19378,6 +19425,19 @@ "node": ">=18" } }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -21837,12 +21897,15 @@ } }, "node_modules/helmet": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.0.0.tgz", - "integrity": "sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", "license": "MIT", "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" } }, "node_modules/hono": { @@ -34613,6 +34676,101 @@ } } }, + "node_modules/webpack-cli": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.1.0.tgz", + "integrity": "sha512-pSJ5p5PkXRD88sfCq5Wo+coc42QykwRu5Md0DyESj0rT6PPPA2wTNabpHPKgqH8EMkfTDo3IWx3iiNXMu8XDBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^1.1.0", + "commander": "^14.0.3", + "cross-spawn": "^7.0.6", + "envinfo": "^7.21.0", + "import-local": "^3.2.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "js-yaml": "^4.0.0 || ^5.0.0", + "json5": "^2.2.3", + "toml": "^3.0.0 || ^4.0.0", + "webpack": "^5.101.0", + "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", + "webpack-dev-server": "^5.0.0" + }, + "peerDependenciesMeta": { + "js-yaml": { + "optional": true + }, + "json5": { + "optional": true + }, + "toml": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/@discoveryjs/json-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/webpack-dev-middleware": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", diff --git a/package.json b/package.json index 9bc8658a6..bc6b86836 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.22.0", + "version": "3.25.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -73,6 +73,7 @@ "@internationalized/number": "3.6.7", "@ionic/angular": "8.8.12", "@keyv/redis": "5.1.6", + "@nest-lab/throttler-storage-redis": "1.2.0", "@nestjs/bull": "11.0.4", "@nestjs/cache-manager": "3.1.3", "@nestjs/common": "11.1.27", @@ -84,6 +85,7 @@ "@nestjs/platform-express": "11.1.27", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", + "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "2.9.1", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", @@ -114,7 +116,7 @@ "fast-redact": "3.5.0", "fuse.js": "7.3.0", "google-spreadsheet": "3.2.0", - "helmet": "7.0.0", + "helmet": "8.2.0", "http-status-codes": "2.3.0", "ionicons": "8.0.13", "jsonpath": "1.3.0", @@ -208,7 +210,8 @@ "ts-jest": "29.4.0", "ts-node": "10.9.2", "tslib": "2.8.1", - "typescript": "5.9.2" + "typescript": "5.9.2", + "webpack-cli": "7.1.0" }, "engines": { "node": ">=22.18.0"