diff --git a/.agents/skills/nestjs-best-practices/AGENTS.md b/.agents/skills/nestjs-best-practices/AGENTS.md new file mode 100644 index 000000000..d14ec242c --- /dev/null +++ b/.agents/skills/nestjs-best-practices/AGENTS.md @@ -0,0 +1,5958 @@ +# NestJS Best Practices + +**Version 1.1.0** +NestJS Best Practices +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring NestJS codebases. Humans may also find it +> useful, but guidance here is optimized for automation and consistency +> by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive best practices and architecture guide for NestJS applications, designed for AI agents and LLMs. Contains 40 rules across 10 categories, prioritized by impact from critical (architecture, dependency injection) to incremental (DevOps patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Architecture](#1-architecture) — **CRITICAL** + - 1.1 [Avoid Circular Dependencies](#11-avoid-circular-dependencies) + - 1.2 [Organize by Feature Modules](#12-organize-by-feature-modules) + - 1.3 [Use Proper Module Sharing Patterns](#13-use-proper-module-sharing-patterns) + - 1.4 [Single Responsibility for Services](#14-single-responsibility-for-services) + - 1.5 [Use Event-Driven Architecture for Decoupling](#15-use-event-driven-architecture-for-decoupling) + - 1.6 [Use Repository Pattern for Data Access](#16-use-repository-pattern-for-data-access) +2. [Dependency Injection](#2-dependency-injection) — **CRITICAL** + - 2.1 [Avoid Service Locator Anti-Pattern](#21-avoid-service-locator-anti-pattern) + - 2.2 [Apply Interface Segregation Principle](#22-apply-interface-segregation-principle) + - 2.3 [Honor Liskov Substitution Principle](#23-honor-liskov-substitution-principle) + - 2.4 [Prefer Constructor Injection](#24-prefer-constructor-injection) + - 2.5 [Understand Provider Scopes](#25-understand-provider-scopes) + - 2.6 [Use Injection Tokens for Interfaces](#26-use-injection-tokens-for-interfaces) +3. [Error Handling](#3-error-handling) — **HIGH** + - 3.1 [Handle Async Errors Properly](#31-handle-async-errors-properly) + - 3.2 [Throw HTTP Exceptions from Services](#32-throw-http-exceptions-from-services) + - 3.3 [Use Exception Filters for Error Handling](#33-use-exception-filters-for-error-handling) +4. [Security](#4-security) — **HIGH** + - 4.1 [Implement Secure JWT Authentication](#41-implement-secure-jwt-authentication) + - 4.2 [Implement Rate Limiting](#42-implement-rate-limiting) + - 4.3 [Sanitize Output to Prevent XSS](#43-sanitize-output-to-prevent-xss) + - 4.4 [Use Guards for Authentication and Authorization](#44-use-guards-for-authentication-and-authorization) + - 4.5 [Validate All Input with DTOs and Pipes](#45-validate-all-input-with-dtos-and-pipes) +5. [Performance](#5-performance) — **HIGH** + - 5.1 [Use Async Lifecycle Hooks Correctly](#51-use-async-lifecycle-hooks-correctly) + - 5.2 [Use Lazy Loading for Large Modules](#52-use-lazy-loading-for-large-modules) + - 5.3 [Optimize Database Queries](#53-optimize-database-queries) + - 5.4 [Use Caching Strategically](#54-use-caching-strategically) +6. [Testing](#6-testing) — **MEDIUM-HIGH** + - 6.1 [Use Supertest for E2E Testing](#61-use-supertest-for-e2e-testing) + - 6.2 [Mock External Services in Tests](#62-mock-external-services-in-tests) + - 6.3 [Use Testing Module for Unit Tests](#63-use-testing-module-for-unit-tests) +7. [Database & ORM](#7-database-orm) — **MEDIUM-HIGH** + - 7.1 [Avoid N+1 Query Problems](#71-avoid-n-1-query-problems) + - 7.2 [Use Database Migrations](#72-use-database-migrations) + - 7.3 [Use Transactions for Multi-Step Operations](#73-use-transactions-for-multi-step-operations) +8. [API Design](#8-api-design) — **MEDIUM** + - 8.1 [Use DTOs and Serialization for API Responses](#81-use-dtos-and-serialization-for-api-responses) + - 8.2 [Use Interceptors for Cross-Cutting Concerns](#82-use-interceptors-for-cross-cutting-concerns) + - 8.3 [Use Pipes for Input Transformation](#83-use-pipes-for-input-transformation) + - 8.4 [Use API Versioning for Breaking Changes](#84-use-api-versioning-for-breaking-changes) +9. [Microservices](#9-microservices) — **MEDIUM** + - 9.1 [Implement Health Checks for Microservices](#91-implement-health-checks-for-microservices) + - 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly) + - 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs) +10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM** + - 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown) + - 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration) + - 10.3 [Use Structured Logging](#103-use-structured-logging) + +--- + +## 1. Architecture + +**Section Impact: CRITICAL** + +### 1.1 Avoid Circular Dependencies + +**Impact: CRITICAL** — "#1 cause of runtime crashes" + +Circular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications. + +**Incorrect (circular module imports):** + +```typescript +// users.module.ts +@Module({ + imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} + +// orders.module.ts +@Module({ + imports: [UsersModule], // Circular dependency! + providers: [OrdersService], + exports: [OrdersService], +}) +export class OrdersModule {} +``` + +**Correct (extract shared logic or use events):** + +```typescript +// Option 1: Extract shared logic to a third module +// shared.module.ts +@Module({ + providers: [SharedService], + exports: [SharedService], +}) +export class SharedModule {} + +// users.module.ts +@Module({ + imports: [SharedModule], + providers: [UsersService], +}) +export class UsersModule {} + +// orders.module.ts +@Module({ + imports: [SharedModule], + providers: [OrdersService], +}) +export class OrdersModule {} + +// Option 2: Use events for decoupled communication +// users.service.ts +@Injectable() +export class UsersService { + constructor(private eventEmitter: EventEmitter2) {} + + async createUser(data: CreateUserDto) { + const user = await this.userRepo.save(data); + this.eventEmitter.emit('user.created', user); + return user; + } +} + +// orders.service.ts +@Injectable() +export class OrdersService { + @OnEvent('user.created') + handleUserCreated(user: User) { + // React to user creation without direct dependency + } +} +``` + +Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency) + +--- + +### 1.2 Organize by Feature Modules + +**Impact: CRITICAL** — "3-5x faster onboarding and development" + +Organize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development. + +**Incorrect (technical layer organization):** + +```typescript +// Technical layer organization (anti-pattern) +src/ +├── controllers/ +│ ├── users.controller.ts +│ ├── orders.controller.ts +│ └── products.controller.ts +├── services/ +│ ├── users.service.ts +│ ├── orders.service.ts +│ └── products.service.ts +├── entities/ +│ ├── user.entity.ts +│ ├── order.entity.ts +│ └── product.entity.ts +└── app.module.ts // Imports everything directly +``` + +**Correct (feature module organization):** + +```typescript +// Feature module organization +src/ +├── users/ +│ ├── dto/ +│ │ ├── create-user.dto.ts +│ │ └── update-user.dto.ts +│ ├── entities/ +│ │ └── user.entity.ts +│ ├── users.controller.ts +│ ├── users.service.ts +│ ├── users.repository.ts +│ └── users.module.ts +├── orders/ +│ ├── dto/ +│ ├── entities/ +│ ├── orders.controller.ts +│ ├── orders.service.ts +│ └── orders.module.ts +├── shared/ +│ ├── guards/ +│ ├── interceptors/ +│ ├── filters/ +│ └── shared.module.ts +└── app.module.ts + +// users.module.ts +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersService], // Only export what others need +}) +export class UsersModule {} + +// app.module.ts +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forRoot(), + UsersModule, + OrdersModule, + SharedModule, + ], +}) +export class AppModule {} +``` + +Reference: [NestJS Modules](https://docs.nestjs.com/modules) + +--- + +### 1.3 Use Proper Module Sharing Patterns + +**Impact: CRITICAL** — Prevents duplicate instances, memory leaks, and state inconsistency + +NestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed. + +**Incorrect (service provided in multiple modules):** + +```typescript +// StorageService provided directly in multiple modules - WRONG +// storage.service.ts +@Injectable() +export class StorageService { + private cache = new Map(); // Each instance has separate state! + + store(key: string, value: any) { + this.cache.set(key, value); + } +} + +// app.module.ts +@Module({ + providers: [StorageService], // Instance #1 + controllers: [AppController], +}) +export class AppModule {} + +// videos.module.ts +@Module({ + providers: [StorageService], // Instance #2 - different from AppModule! + controllers: [VideosController], +}) +export class VideosModule {} + +// Problems: +// 1. Two separate StorageService instances exist +// 2. cache.set() in VideosModule doesn't affect AppModule's cache +// 3. Memory wasted on duplicate instances +// 4. Debugging nightmares when state doesn't sync +``` + +**Correct (dedicated module with exports):** + +```typescript +// storage/storage.module.ts +@Module({ + providers: [StorageService], + exports: [StorageService], // Make available to importers +}) +export class StorageModule {} + +// videos/videos.module.ts +@Module({ + imports: [StorageModule], // Import the module, not the service + controllers: [VideosController], + providers: [VideosService], +}) +export class VideosModule {} + +// channels/channels.module.ts +@Module({ + imports: [StorageModule], // Same instance shared + controllers: [ChannelsController], + providers: [ChannelsService], +}) +export class ChannelsModule {} + +// app.module.ts +@Module({ + imports: [ + StorageModule, // Only if AppModule itself needs StorageService + VideosModule, + ChannelsModule, + ], +}) +export class AppModule {} + +// Now all modules share the SAME StorageService instance +``` + +**When to use @Global() (sparingly):** + +```typescript +// ONLY for truly cross-cutting concerns +@Global() +@Module({ + providers: [ConfigService, LoggerService], + exports: [ConfigService, LoggerService], +}) +export class CoreModule {} + +// Import once in AppModule +@Module({ + imports: [CoreModule], // Registered globally, available everywhere +}) +export class AppModule {} + +// Other modules don't need to import CoreModule +@Module({ + controllers: [UsersController], + providers: [UsersService], // Can inject ConfigService without importing +}) +export class UsersModule {} + +// WARNING: Don't make everything global! +// - Hides dependencies (can't see what a module needs from imports) +// - Makes testing harder +// - Reserve for: config, logging, database connections +``` + +**Module re-exporting pattern:** + +```typescript +// common.module.ts - shared utilities +@Module({ + providers: [DateService, ValidationService], + exports: [DateService, ValidationService], +}) +export class CommonModule {} + +// core.module.ts - re-exports common for convenience +@Module({ + imports: [CommonModule, DatabaseModule], + exports: [CommonModule, DatabaseModule], // Re-export for consumers +}) +export class CoreModule {} + +// feature.module.ts - imports CoreModule, gets both +@Module({ + imports: [CoreModule], // Gets CommonModule + DatabaseModule + controllers: [FeatureController], +}) +export class FeatureModule {} +``` + +Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules) + +--- + +### 1.4 Single Responsibility for Services + +**Impact: CRITICAL** — "40%+ improvement in testability" + +Each service should have a single, well-defined responsibility. Avoid "god services" that handle multiple unrelated concerns. If a service name includes "And" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+. + +**Incorrect (god service anti-pattern):** + +```typescript +// God service anti-pattern +@Injectable() +export class UserAndOrderService { + constructor( + private userRepo: UserRepository, + private orderRepo: OrderRepository, + private mailer: MailService, + private payment: PaymentService, + ) {} + + async createUser(dto: CreateUserDto) { + const user = await this.userRepo.save(dto); + await this.mailer.sendWelcome(user); + return user; + } + + async createOrder(userId: string, dto: CreateOrderDto) { + const order = await this.orderRepo.save({ userId, ...dto }); + await this.payment.charge(order); + await this.mailer.sendOrderConfirmation(order); + return order; + } + + async calculateOrderStats(userId: string) { + // Stats logic mixed in + } + + async validatePayment(orderId: string) { + // Payment logic mixed in + } +} +``` + +**Correct (focused services with single responsibility):** + +```typescript +// Focused services with single responsibility +@Injectable() +export class UsersService { + constructor(private userRepo: UserRepository) {} + + async create(dto: CreateUserDto): Promise { + return this.userRepo.save(dto); + } + + async findById(id: string): Promise { + return this.userRepo.findOneOrFail({ where: { id } }); + } +} + +@Injectable() +export class OrdersService { + constructor(private orderRepo: OrderRepository) {} + + async create(userId: string, dto: CreateOrderDto): Promise { + return this.orderRepo.save({ userId, ...dto }); + } + + async findByUser(userId: string): Promise { + return this.orderRepo.find({ where: { userId } }); + } +} + +@Injectable() +export class OrderStatsService { + constructor(private orderRepo: OrderRepository) {} + + async calculateForUser(userId: string): Promise { + // Focused stats calculation + } +} + +// Orchestration in controller or dedicated orchestrator +@Controller('orders') +export class OrdersController { + constructor( + private orders: OrdersService, + private payment: PaymentService, + private notifications: NotificationService, + ) {} + + @Post() + async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) { + const order = await this.orders.create(user.id, dto); + await this.payment.charge(order); + await this.notifications.sendOrderConfirmation(order); + return order; + } +} +``` + +Reference: [NestJS Providers](https://docs.nestjs.com/providers) + +--- + +### 1.5 Use Event-Driven Architecture for Decoupling + +**Impact: MEDIUM-HIGH** — Enables async processing and modularity + +Use `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing. + +**Incorrect (direct service coupling):** + +```typescript +// Direct service coupling +@Injectable() +export class OrdersService { + constructor( + private inventoryService: InventoryService, + private emailService: EmailService, + private analyticsService: AnalyticsService, + private notificationService: NotificationService, + private loyaltyService: LoyaltyService, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Tight coupling - OrdersService knows about all consumers + await this.inventoryService.reserve(order.items); + await this.emailService.sendConfirmation(order); + await this.analyticsService.track('order_created', order); + await this.notificationService.push(order.userId, 'Order placed'); + await this.loyaltyService.addPoints(order.userId, order.total); + + // Adding new behavior requires modifying this service + return order; + } +} +``` + +**Correct (event-driven decoupling):** + +```typescript +// Use EventEmitter for decoupling +import { EventEmitter2 } from '@nestjs/event-emitter'; + +// Define event +export class OrderCreatedEvent { + constructor( + public readonly orderId: string, + public readonly userId: string, + public readonly items: OrderItem[], + public readonly total: number, + ) {} +} + +// Service emits events +@Injectable() +export class OrdersService { + constructor( + private eventEmitter: EventEmitter2, + private repo: Repository, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Emit event - no knowledge of consumers + this.eventEmitter.emit( + 'order.created', + new OrderCreatedEvent(order.id, order.userId, order.items, order.total), + ); + + return order; + } +} + +// Listeners in separate modules +@Injectable() +export class InventoryListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.inventoryService.reserve(event.items); + } +} + +@Injectable() +export class EmailListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.emailService.sendConfirmation(event.orderId); + } +} + +@Injectable() +export class AnalyticsListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.analyticsService.track('order_created', { + orderId: event.orderId, + total: event.total, + }); + } +} +``` + +Reference: [NestJS Events](https://docs.nestjs.com/techniques/events) + +--- + +### 1.6 Use Repository Pattern for Data Access + +**Impact: HIGH** — Decouples business logic from database + +Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code. + +**Incorrect (complex queries in services):** + +```typescript +// Complex queries in services +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) private repo: Repository, + ) {} + + async findActiveWithOrders(minOrders: number): Promise { + // Complex query logic mixed with business logic + return this.repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.orders', 'order') + .where('user.isActive = :active', { active: true }) + .andWhere('user.deletedAt IS NULL') + .groupBy('user.id') + .having('COUNT(order.id) >= :min', { min: minOrders }) + .orderBy('user.createdAt', 'DESC') + .getMany(); + } + + // Service becomes bloated with query logic +} +``` + +**Correct (custom repository with encapsulated queries):** + +```typescript +// Custom repository with encapsulated queries +@Injectable() +export class UsersRepository { + constructor( + @InjectRepository(User) private repo: Repository, + ) {} + + async findById(id: string): Promise { + return this.repo.findOne({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.repo.findOne({ where: { email } }); + } + + async findActiveWithMinOrders(minOrders: number): Promise { + return this.repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.orders', 'order') + .where('user.isActive = :active', { active: true }) + .andWhere('user.deletedAt IS NULL') + .groupBy('user.id') + .having('COUNT(order.id) >= :min', { min: minOrders }) + .orderBy('user.createdAt', 'DESC') + .getMany(); + } + + async save(user: User): Promise { + return this.repo.save(user); + } +} + +// Clean service with business logic only +@Injectable() +export class UsersService { + constructor(private usersRepo: UsersRepository) {} + + async getActiveUsersWithOrders(): Promise { + return this.usersRepo.findActiveWithMinOrders(1); + } + + async create(dto: CreateUserDto): Promise { + const existing = await this.usersRepo.findByEmail(dto.email); + if (existing) { + throw new ConflictException('Email already registered'); + } + + const user = new User(); + user.email = dto.email; + user.name = dto.name; + return this.usersRepo.save(user); + } +} +``` + +Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html) + +--- + +## 2. Dependency Injection + +**Section Impact: CRITICAL** + +### 2.1 Avoid Service Locator Anti-Pattern + +**Impact: HIGH** — Hides dependencies and breaks testability + +Avoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead. + +**Incorrect (service locator anti-pattern):** + +```typescript +// Use ModuleRef to get dependencies dynamically +@Injectable() +export class OrdersService { + constructor(private moduleRef: ModuleRef) {} + + async createOrder(dto: CreateOrderDto): Promise { + // Dependencies are hidden - not visible in constructor + const usersService = this.moduleRef.get(UsersService); + const inventoryService = this.moduleRef.get(InventoryService); + const paymentService = this.moduleRef.get(PaymentService); + + const user = await usersService.findOne(dto.userId); + // ... rest of logic + } +} + +// Global singleton container +class ServiceContainer { + private static instance: ServiceContainer; + private services = new Map(); + + static getInstance(): ServiceContainer { + if (!this.instance) { + this.instance = new ServiceContainer(); + } + return this.instance; + } + + get(key: string): T { + return this.services.get(key); + } +} +``` + +**Correct (constructor injection with explicit dependencies):** + +```typescript +// Use constructor injection - dependencies are explicit +@Injectable() +export class OrdersService { + constructor( + private usersService: UsersService, + private inventoryService: InventoryService, + private paymentService: PaymentService, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const user = await this.usersService.findOne(dto.userId); + const inventory = await this.inventoryService.check(dto.items); + // Dependencies are clear and testable + } +} + +// Easy to test with mocks +describe('OrdersService', () => { + let service: OrdersService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + OrdersService, + { provide: UsersService, useValue: mockUsersService }, + { provide: InventoryService, useValue: mockInventoryService }, + { provide: PaymentService, useValue: mockPaymentService }, + ], + }).compile(); + + service = module.get(OrdersService); + }); +}); + +// VALID: Factory pattern for dynamic instantiation +@Injectable() +export class HandlerFactory { + constructor(private moduleRef: ModuleRef) {} + + getHandler(type: string): Handler { + switch (type) { + case 'email': + return this.moduleRef.get(EmailHandler); + case 'sms': + return this.moduleRef.get(SmsHandler); + default: + return this.moduleRef.get(DefaultHandler); + } + } +} +``` + +Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref) + +--- + +### 2.2 Apply Interface Segregation Principle + +**Impact: HIGH** — Reduces coupling and improves testability by 30-50% + +Clients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating "fat" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones. + +**Incorrect (fat interface forcing unused dependencies):** + +```typescript +// Fat interface - forces all consumers to depend on everything +interface NotificationService { + sendEmail(to: string, subject: string, body: string): Promise; + sendSms(phone: string, message: string): Promise; + sendPush(userId: string, notification: PushPayload): Promise; + sendSlack(channel: string, message: string): Promise; + logNotification(type: string, payload: any): Promise; + getDeliveryStatus(id: string): Promise; + retryFailed(id: string): Promise; + scheduleNotification(dto: ScheduleDto): Promise; +} + +// Consumer only needs email, but must mock everything for tests +@Injectable() +export class OrdersService { + constructor( + private notifications: NotificationService, // Depends on 8 methods, uses 1 + ) {} + + async confirmOrder(order: Order): Promise { + await this.notifications.sendEmail( + order.customer.email, + 'Order Confirmed', + `Your order ${order.id} has been confirmed.`, + ); + } +} + +// Testing is painful - must mock unused methods +const mockNotificationService = { + sendEmail: jest.fn(), + sendSms: jest.fn(), // Never used, but required + sendPush: jest.fn(), // Never used, but required + sendSlack: jest.fn(), // Never used, but required + logNotification: jest.fn(), // Never used, but required + getDeliveryStatus: jest.fn(), // Never used, but required + retryFailed: jest.fn(), // Never used, but required + scheduleNotification: jest.fn(), // Never used, but required +}; +``` + +**Correct (segregated interfaces by capability):** + +```typescript +// Segregated interfaces - each focused on one capability +interface EmailSender { + sendEmail(to: string, subject: string, body: string): Promise; +} + +interface SmsSender { + sendSms(phone: string, message: string): Promise; +} + +interface PushSender { + sendPush(userId: string, notification: PushPayload): Promise; +} + +interface NotificationLogger { + logNotification(type: string, payload: any): Promise; +} + +interface NotificationScheduler { + scheduleNotification(dto: ScheduleDto): Promise; +} + +// Implementation can implement multiple interfaces +@Injectable() +export class NotificationService implements EmailSender, SmsSender, PushSender { + async sendEmail(to: string, subject: string, body: string): Promise { + // Email implementation + } + + async sendSms(phone: string, message: string): Promise { + // SMS implementation + } + + async sendPush(userId: string, notification: PushPayload): Promise { + // Push implementation + } +} + +// Or separate implementations +@Injectable() +export class SendGridEmailService implements EmailSender { + async sendEmail(to: string, subject: string, body: string): Promise { + // SendGrid-specific implementation + } +} + +// Consumer depends only on what it needs +@Injectable() +export class OrdersService { + constructor( + @Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency + ) {} + + async confirmOrder(order: Order): Promise { + await this.emailSender.sendEmail( + order.customer.email, + 'Order Confirmed', + `Your order ${order.id} has been confirmed.`, + ); + } +} + +// Testing is simple - only mock what's used +const mockEmailSender: EmailSender = { + sendEmail: jest.fn(), +}; + +// Module registration with tokens +export const EMAIL_SENDER = Symbol('EMAIL_SENDER'); +export const SMS_SENDER = Symbol('SMS_SENDER'); + +@Module({ + providers: [ + { provide: EMAIL_SENDER, useClass: SendGridEmailService }, + { provide: SMS_SENDER, useClass: TwilioSmsService }, + ], + exports: [EMAIL_SENDER, SMS_SENDER], +}) +export class NotificationModule {} +``` + +**Combining interfaces when needed:** + +```typescript +// Sometimes a consumer legitimately needs multiple capabilities +interface EmailAndSmsSender extends EmailSender, SmsSender {} + +// Or use intersection types +type MultiChannelSender = EmailSender & SmsSender & PushSender; + +// Consumer that genuinely needs multiple channels +@Injectable() +export class AlertService { + constructor( + @Inject(MULTI_CHANNEL_SENDER) + private sender: EmailSender & SmsSender, + ) {} + + async sendCriticalAlert(user: User, message: string): Promise { + await Promise.all([ + this.sender.sendEmail(user.email, 'Critical Alert', message), + this.sender.sendSms(user.phone, message), + ]); + } +} +``` + +Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle) + +--- + +### 2.3 Honor Liskov Substitution Principle + +**Impact: HIGH** — Ensures implementations are truly interchangeable without breaking callers + +Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations. + +**Incorrect (implementation violates the contract):** + +```typescript +// Base interface with clear contract +interface PaymentGateway { + /** + * Charges the specified amount. + * @returns PaymentResult on success + * @throws PaymentFailedException on payment failure + */ + charge(amount: number, currency: string): Promise; +} + +// Production implementation - follows the contract +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + const response = await this.stripe.charges.create({ amount, currency }); + return { success: true, transactionId: response.id, amount }; + } +} + +// Mock that violates LSP - different behavior! +@Injectable() +export class MockPaymentService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + // VIOLATION 1: Throws for valid input (contract says return PaymentResult) + if (amount > 1000) { + throw new Error('Mock does not support large amounts'); + } + + // VIOLATION 2: Returns null instead of PaymentResult + if (currency !== 'USD') { + return null as any; // Real service would convert or reject properly + } + + // VIOLATION 3: Missing required field + return { success: true } as PaymentResult; // Missing transactionId! + } +} + +// Consumer trusts the contract +@Injectable() +export class OrdersService { + constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} + + async checkout(order: Order): Promise { + const result = await this.payment.charge(order.total, order.currency); + // These fail with MockPaymentService: + await this.saveTransaction(result.transactionId); // undefined! + await this.sendReceipt(result); // might be null! + } +} +``` + +**Correct (implementations honor the contract):** + +```typescript +// Well-defined interface with documented behavior +interface PaymentGateway { + /** + * Charges the specified amount. + * @param amount - Amount in smallest currency unit (cents) + * @param currency - ISO 4217 currency code + * @returns PaymentResult with transactionId, success status, and amount + * @throws PaymentFailedException if charge is declined + * @throws InvalidCurrencyException if currency is not supported + */ + charge(amount: number, currency: string): Promise; + + /** + * Refunds a previous charge. + * @throws TransactionNotFoundException if transactionId is invalid + */ + refund(transactionId: string, amount?: number): Promise; +} + +// Production implementation +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + try { + const response = await this.stripe.charges.create({ amount, currency }); + return { + success: true, + transactionId: response.id, + amount: response.amount, + }; + } catch (error) { + if (error.type === 'card_error') { + throw new PaymentFailedException(error.message); + } + throw error; + } + } + + async refund(transactionId: string, amount?: number): Promise { + // Implementation... + } +} + +// Mock that honors LSP - same contract, same behavior shape +@Injectable() +export class MockPaymentService implements PaymentGateway { + private transactions = new Map(); + + async charge(amount: number, currency: string): Promise { + // Honor the contract: validate currency like real service would + if (!['USD', 'EUR', 'GBP'].includes(currency)) { + throw new InvalidCurrencyException(`Unsupported currency: ${currency}`); + } + + // Simulate decline for specific test scenarios + if (amount === 99999) { + throw new PaymentFailedException('Card declined (test scenario)'); + } + + // Return same shape as production + const result: PaymentResult = { + success: true, + transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`, + amount, + }; + + this.transactions.set(result.transactionId, result); + return result; + } + + async refund(transactionId: string, amount?: number): Promise { + // Honor the contract: throw if transaction not found + if (!this.transactions.has(transactionId)) { + throw new TransactionNotFoundException(transactionId); + } + + return { + success: true, + refundId: `refund_${transactionId}`, + amount: amount ?? this.transactions.get(transactionId)!.amount, + }; + } +} + +// Consumer can swap implementations safely +@Injectable() +export class OrdersService { + constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} + + async checkout(order: Order): Promise { + try { + const result = await this.payment.charge(order.total, order.currency); + // Works with both StripeService and MockPaymentService + order.transactionId = result.transactionId; + order.status = 'paid'; + return order; + } catch (error) { + if (error instanceof PaymentFailedException) { + order.status = 'payment_failed'; + return order; + } + throw error; + } + } +} +``` + +**Testing LSP compliance:** + +```typescript +// Shared test suite that any implementation must pass +function testPaymentGatewayContract( + createGateway: () => PaymentGateway, +) { + describe('PaymentGateway contract', () => { + let gateway: PaymentGateway; + + beforeEach(() => { + gateway = createGateway(); + }); + + it('returns PaymentResult with all required fields', async () => { + const result = await gateway.charge(1000, 'USD'); + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('transactionId'); + expect(result).toHaveProperty('amount'); + expect(typeof result.transactionId).toBe('string'); + }); + + it('throws InvalidCurrencyException for unsupported currency', async () => { + await expect(gateway.charge(1000, 'INVALID')) + .rejects.toThrow(InvalidCurrencyException); + }); + + it('throws TransactionNotFoundException for invalid refund', async () => { + await expect(gateway.refund('nonexistent')) + .rejects.toThrow(TransactionNotFoundException); + }); + }); +} + +// Run against all implementations +describe('StripeService', () => { + testPaymentGatewayContract(() => new StripeService(mockStripeClient)); +}); + +describe('MockPaymentService', () => { + testPaymentGatewayContract(() => new MockPaymentService()); +}); +``` + +Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) + +--- + +### 2.4 Prefer Constructor Injection + +**Impact: CRITICAL** — Required for proper DI and testing + +Always use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support. + +**Incorrect (property injection with hidden dependencies):** + +```typescript +// Property injection - avoid unless necessary +@Injectable() +export class UsersService { + @Inject() + private userRepo: UserRepository; // Hidden dependency + + @Inject('CONFIG') + private config: ConfigType; // Also hidden + + async findAll() { + return this.userRepo.find(); + } +} + +// Problems: +// 1. Dependencies not visible in constructor +// 2. Service can be instantiated without dependencies in tests +// 3. TypeScript can't enforce dependency types at instantiation +``` + +**Correct (constructor injection with explicit dependencies):** + +```typescript +// Constructor injection - explicit and testable +@Injectable() +export class UsersService { + constructor( + private readonly userRepo: UserRepository, + @Inject('CONFIG') private readonly config: ConfigType, + ) {} + + async findAll(): Promise { + return this.userRepo.find(); + } +} + +// Testing is straightforward +describe('UsersService', () => { + let service: UsersService; + let mockRepo: jest.Mocked; + + beforeEach(() => { + mockRepo = { + find: jest.fn(), + save: jest.fn(), + } as any; + + service = new UsersService(mockRepo, { dbUrl: 'test' }); + }); + + it('should find all users', async () => { + mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]); + const result = await service.findAll(); + expect(result).toHaveLength(1); + }); +}); + +// Only use property injection for optional dependencies +@Injectable() +export class LoggingService { + @Optional() + @Inject('ANALYTICS') + private analytics?: AnalyticsService; + + log(message: string) { + console.log(message); + this.analytics?.track('log', message); // Optional enhancement + } +} +``` + +Reference: [NestJS Providers](https://docs.nestjs.com/providers) + +--- + +### 2.5 Understand Provider Scopes + +**Impact: CRITICAL** — Prevents data leaks and performance issues + +NestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing. + +**Incorrect (wrong scope usage):** + +```typescript +// Request-scoped when not needed (performance hit) +@Injectable({ scope: Scope.REQUEST }) +export class UsersService { + // This creates a new instance for EVERY request + // All dependencies also become request-scoped + async findAll() { + return this.userRepo.find(); + } +} + +// Singleton with mutable request state +@Injectable() // Default: singleton +export class RequestContextService { + private userId: string; // DANGER: Shared across all requests! + + setUser(userId: string) { + this.userId = userId; // Overwrites for all concurrent requests + } + + getUser() { + return this.userId; // Returns wrong user! + } +} +``` + +**Correct (appropriate scope for each use case):** + +```typescript +// Singleton for stateless services (default, most common) +@Injectable() +export class UsersService { + constructor(private readonly userRepo: UserRepository) {} + + async findById(id: string): Promise { + return this.userRepo.findOne({ where: { id } }); + } +} + +// Request-scoped ONLY when you need request context +@Injectable({ scope: Scope.REQUEST }) +export class RequestContextService { + private userId: string; + + setUser(userId: string) { + this.userId = userId; + } + + getUser(): string { + return this.userId; + } +} + +// Better: Use NestJS built-in request context +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; + +@Injectable({ scope: Scope.REQUEST }) +export class AuditService { + constructor(@Inject(REQUEST) private request: Request) {} + + log(action: string) { + console.log(`User ${this.request.user?.id} performed ${action}`); + } +} + +// Best: Use ClsModule for async context (no scope bubble-up) +import { ClsService } from 'nestjs-cls'; + +@Injectable() // Stays singleton! +export class AuditService { + constructor(private cls: ClsService) {} + + log(action: string) { + const userId = this.cls.get('userId'); + console.log(`User ${userId} performed ${action}`); + } +} +``` + +Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes) + +--- + +### 2.6 Use Injection Tokens for Interfaces + +**Impact: HIGH** — Enables interface-based DI at runtime + +TypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments. + +**Incorrect (interface can't be used as token):** + +```typescript +// Interface can't be used as injection token +interface PaymentGateway { + charge(amount: number): Promise; +} + +@Injectable() +export class StripeService implements PaymentGateway { + charge(amount: number) { /* ... */ } +} + +@Injectable() +export class OrdersService { + // This WON'T work - PaymentGateway doesn't exist at runtime + constructor(private payment: PaymentGateway) {} +} +``` + +**Correct (symbol tokens or abstract classes):** + +```typescript +// Option 1: String/Symbol tokens (most flexible) +export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY'); + +export interface PaymentGateway { + charge(amount: number): Promise; +} + +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number): Promise { + // Stripe implementation + } +} + +@Injectable() +export class MockPaymentService implements PaymentGateway { + async charge(amount: number): Promise { + return { success: true, id: 'mock-id' }; + } +} + +// Module registration +@Module({ + providers: [ + { + provide: PAYMENT_GATEWAY, + useClass: process.env.NODE_ENV === 'test' + ? MockPaymentService + : StripeService, + }, + ], + exports: [PAYMENT_GATEWAY], +}) +export class PaymentModule {} + +// Injection +@Injectable() +export class OrdersService { + constructor( + @Inject(PAYMENT_GATEWAY) private payment: PaymentGateway, + ) {} + + async createOrder(dto: CreateOrderDto) { + await this.payment.charge(dto.amount); + } +} + +// Option 2: Abstract class (carries runtime type info) +export abstract class PaymentGateway { + abstract charge(amount: number): Promise; +} + +@Injectable() +export class StripeService extends PaymentGateway { + async charge(amount: number): Promise { + // Implementation + } +} + +// No @Inject needed with abstract class +@Injectable() +export class OrdersService { + constructor(private payment: PaymentGateway) {} +} +``` + +Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers) + +--- + +## 3. Error Handling + +**Section Impact: HIGH** + +### 3.1 Handle Async Errors Properly + +**Impact: HIGH** — Prevents process crashes from unhandled rejections + +NestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net. + +**Incorrect (fire-and-forget without error handling):** + +```typescript +// Fire-and-forget without error handling +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Fire and forget - if this fails, error is unhandled! + this.emailService.sendWelcome(user.email); + + return user; + } +} + +// Unhandled promise in event handler +@Injectable() +export class OrdersService { + @OnEvent('order.created') + handleOrderCreated(event: OrderCreatedEvent) { + // This returns a promise but it's not awaited! + this.processOrder(event); + // Errors will crash the process + } + + private async processOrder(event: OrderCreatedEvent): Promise { + await this.inventoryService.reserve(event.items); + await this.notificationService.send(event.userId); + } +} + +// Missing try-catch in scheduled tasks +@Cron('0 0 * * *') +async dailyCleanup(): Promise { + await this.cleanupService.run(); + // If this throws, no error handling +} +``` + +**Correct (explicit async error handling):** + +```typescript +// Handle fire-and-forget with explicit catch +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Explicitly catch and log errors + this.emailService.sendWelcome(user.email).catch((error) => { + this.logger.error('Failed to send welcome email', error.stack); + // Optionally queue for retry + }); + + return user; + } +} + +// Properly handle async event handlers +@Injectable() +export class OrdersService { + private readonly logger = new Logger(OrdersService.name); + + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + try { + await this.processOrder(event); + } catch (error) { + this.logger.error('Failed to process order', { event, error }); + // Don't rethrow - would crash the process + await this.deadLetterQueue.add('order.created', event); + } + } +} + +// Safe scheduled tasks +@Injectable() +export class CleanupService { + private readonly logger = new Logger(CleanupService.name); + + @Cron('0 0 * * *') + async dailyCleanup(): Promise { + try { + await this.cleanupService.run(); + this.logger.log('Daily cleanup completed'); + } catch (error) { + this.logger.error('Daily cleanup failed', error.stack); + // Alert or retry logic + } + } +} + +// Global unhandled rejection handler in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const logger = new Logger('Bootstrap'); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + }); + + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + process.exit(1); + }); + + await app.listen(3000); +} +``` + +Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection) + +--- + +### 3.2 Throw HTTP Exceptions from Services + +**Impact: HIGH** — Keeps controllers thin and simplifies error handling + +It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes. + +**Incorrect (return error objects instead of throwing):** + +```typescript +// Return error objects instead of throwing +@Injectable() +export class UsersService { + async findById(id: string): Promise<{ user?: User; error?: string }> { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + return { error: 'User not found' }; // Controller must check this + } + return { user }; + } +} + +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string) { + const result = await this.usersService.findById(id); + if (result.error) { + throw new NotFoundException(result.error); + } + return result.user; + } +} +``` + +**Correct (throw exceptions directly from service):** + +```typescript +// Throw exceptions directly from service +@Injectable() +export class UsersService { + constructor(private readonly repo: UserRepository) {} + + async findById(id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } + + async create(dto: CreateUserDto): Promise { + const existing = await this.repo.findOne({ + where: { email: dto.email }, + }); + if (existing) { + throw new ConflictException('Email already registered'); + } + return this.repo.save(dto); + } + + async update(id: string, dto: UpdateUserDto): Promise { + const user = await this.findById(id); // Throws if not found + Object.assign(user, dto); + return this.repo.save(user); + } +} + +// Controller stays thin +@Controller('users') +export class UsersController { + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + } + + @Post() + create(@Body() dto: CreateUserDto): Promise { + return this.usersService.create(dto); + } +} + +// For layer-agnostic services, use domain exceptions +export class EntityNotFoundException extends Error { + constructor( + public readonly entity: string, + public readonly id: string, + ) { + super(`${entity} with ID "${id}" not found`); + } +} + +// Map to HTTP in exception filter +@Catch(EntityNotFoundException) +export class EntityNotFoundFilter implements ExceptionFilter { + catch(exception: EntityNotFoundException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + response.status(404).json({ + statusCode: 404, + message: exception.message, + entity: exception.entity, + id: exception.id, + }); + } +} +``` + +Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters) + +--- + +### 3.3 Use Exception Filters for Error Handling + +**Impact: HIGH** — Consistent, centralized error handling + +Never catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions. + +**Incorrect (manual error handling in controllers):** + +```typescript +// Manual error handling in controllers +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string, @Res() res: Response) { + try { + const user = await this.usersService.findById(id); + if (!user) { + return res.status(404).json({ + statusCode: 404, + message: 'User not found', + }); + } + return res.json(user); + } catch (error) { + console.error(error); + return res.status(500).json({ + statusCode: 500, + message: 'Internal server error', + }); + } + } +} +``` + +**Correct (exception filters with consistent handling):** + +```typescript +// Use built-in and custom exceptions +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findById(id); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } +} + +// Custom domain exception +export class UserNotFoundException extends NotFoundException { + constructor(userId: string) { + super({ + statusCode: 404, + error: 'Not Found', + message: `User with ID "${userId}" not found`, + code: 'USER_NOT_FOUND', + }); + } +} + +// Custom exception filter for domain errors +@Catch(DomainException) +export class DomainExceptionFilter implements ExceptionFilter { + catch(exception: DomainException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = exception.getStatus?.() || 400; + + response.status(status).json({ + statusCode: status, + code: exception.code, + message: exception.message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} + +// Global exception filter for unhandled errors +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + constructor(private readonly logger: Logger) {} + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.message + : 'Internal server error'; + + this.logger.error( + `${request.method} ${request.url}`, + exception instanceof Error ? exception.stack : exception, + ); + + response.status(status).json({ + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} + +// Register globally in main.ts +app.useGlobalFilters( + new AllExceptionsFilter(app.get(Logger)), + new DomainExceptionFilter(), +); + +// Or via module +@Module({ + providers: [ + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, + ], +}) +export class AppModule {} +``` + +Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters) + +--- + +## 4. Security + +**Section Impact: HIGH** + +### 4.1 Implement Secure JWT Authentication + +**Impact: CRITICAL** — Essential for secure APIs + +Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads. + +**Incorrect (insecure JWT implementation):** + +```typescript +// Hardcode secrets +@Module({ + imports: [ + JwtModule.register({ + secret: 'my-secret-key', // Exposed in code + signOptions: { expiresIn: '7d' }, // Too long + }), + ], +}) +export class AuthModule {} + +// Store sensitive data in JWT +async login(user: User): Promise<{ accessToken: string }> { + const payload = { + sub: user.id, + email: user.email, + password: user.password, // NEVER include password! + ssn: user.ssn, // NEVER include sensitive data! + isAdmin: user.isAdmin, // Can be tampered if not verified + }; + return { accessToken: this.jwtService.sign(payload) }; +} + +// Skip token validation +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: 'my-secret', + }); + } + + async validate(payload: any): Promise { + return payload; // No validation of user existence + } +} +``` + +**Correct (secure JWT with refresh tokens):** + +```typescript +// Secure JWT configuration +@Module({ + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { + expiresIn: '15m', // Short-lived access tokens + issuer: config.get('JWT_ISSUER'), + audience: config.get('JWT_AUDIENCE'), + }, + }), + }), + PassportModule.register({ defaultStrategy: 'jwt' }), + ], +}) +export class AuthModule {} + +// Minimal JWT payload +@Injectable() +export class AuthService { + async login(user: User): Promise { + // Only include necessary, non-sensitive data + const payload: JwtPayload = { + sub: user.id, + email: user.email, + roles: user.roles, + iat: Math.floor(Date.now() / 1000), + }; + + const accessToken = this.jwtService.sign(payload); + const refreshToken = await this.createRefreshToken(user.id); + + return { accessToken, refreshToken, expiresIn: 900 }; + } + + private async createRefreshToken(userId: string): Promise { + const token = randomBytes(32).toString('hex'); + const hashedToken = await bcrypt.hash(token, 10); + + await this.refreshTokenRepo.save({ + userId, + token: hashedToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }); + + return token; + } +} + +// Proper JWT strategy with validation +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private config: ConfigService, + private usersService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.get('JWT_SECRET'), + ignoreExpiration: false, + issuer: config.get('JWT_ISSUER'), + audience: config.get('JWT_AUDIENCE'), + }); + } + + async validate(payload: JwtPayload): Promise { + // Verify user still exists and is active + const user = await this.usersService.findById(payload.sub); + + if (!user || !user.isActive) { + throw new UnauthorizedException('User not found or inactive'); + } + + // Verify token wasn't issued before password change + if (user.passwordChangedAt) { + const tokenIssuedAt = new Date(payload.iat * 1000); + if (tokenIssuedAt < user.passwordChangedAt) { + throw new UnauthorizedException('Token invalidated by password change'); + } + } + + return user; + } +} +``` + +Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication) + +--- + +### 4.2 Implement Rate Limiting + +**Impact: HIGH** — Protects against abuse and ensures fair resource usage + +Use `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments. + +**Incorrect (no rate limiting on sensitive endpoints):** + +```typescript +// No rate limiting on sensitive endpoints +@Controller('auth') +export class AuthController { + @Post('login') + async login(@Body() dto: LoginDto): Promise { + // Attackers can brute-force credentials + return this.authService.login(dto); + } + + @Post('forgot-password') + async forgotPassword(@Body() dto: ForgotPasswordDto): Promise { + // Can be abused to spam users with emails + return this.authService.sendResetEmail(dto.email); + } +} + +// Same limits for all endpoints +@UseGuards(ThrottlerGuard) +@Controller('api') +export class ApiController { + @Get('public-data') + async getPublic() {} // Should allow more requests + + @Post('process-payment') + async payment() {} // Should be more restrictive +} +``` + +**Correct (configured throttler with endpoint-specific limits):** + +```typescript +// Configure throttler globally with multiple limits +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; + +@Module({ + imports: [ + ThrottlerModule.forRoot([ + { + name: 'short', + ttl: 1000, // 1 second + limit: 3, // 3 requests per second + }, + { + name: 'medium', + ttl: 10000, // 10 seconds + limit: 20, // 20 requests per 10 seconds + }, + { + name: 'long', + ttl: 60000, // 1 minute + limit: 100, // 100 requests per minute + }, + ]), + ], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], +}) +export class AppModule {} + +// Override limits per endpoint +@Controller('auth') +export class AuthController { + @Post('login') + @Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute + async login(@Body() dto: LoginDto): Promise { + return this.authService.login(dto); + } + + @Post('forgot-password') + @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour + async forgotPassword(@Body() dto: ForgotPasswordDto): Promise { + return this.authService.sendResetEmail(dto.email); + } +} + +// Skip throttling for certain routes +@Controller('health') +export class HealthController { + @Get() + @SkipThrottle() + check(): string { + return 'OK'; + } +} + +// Custom throttle per user type +@Injectable() +export class CustomThrottlerGuard extends ThrottlerGuard { + protected async getTracker(req: Request): Promise { + // Use user ID if authenticated, IP otherwise + return req.user?.id || req.ip; + } + + protected async getLimit(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + // Higher limits for authenticated users + if (request.user) { + return request.user.isPremium ? 1000 : 200; + } + + return 50; // Anonymous users + } +} +``` + +Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting) + +--- + +### 4.3 Sanitize Output to Prevent XSS + +**Impact: HIGH** — XSS vulnerabilities can compromise user sessions and data + +While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers. + +**Incorrect (storing raw HTML without sanitization):** + +```typescript +// Store raw HTML from users +@Injectable() +export class CommentsService { + async create(dto: CreateCommentDto): Promise { + // User can inject: + return this.repo.save({ + content: dto.content, // Raw, unsanitized + authorId: dto.authorId, + }); + } +} + +// Return HTML without sanitization +@Controller('pages') +export class PagesController { + @Get(':slug') + @Header('Content-Type', 'text/html') + async getPage(@Param('slug') slug: string): Promise { + const page = await this.pagesService.findBySlug(slug); + // If page.content contains user input, XSS is possible + return `${page.content}`; + } +} + +// Reflect user input in errors +@Get(':id') +async findOne(@Param('id') id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + // XSS if id contains malicious content and error is rendered + throw new NotFoundException(`User ${id} not found`); + } + return user; +} +``` + +**Correct (sanitize content and use proper headers):** + +```typescript +// Sanitize HTML content before storage +import * as sanitizeHtml from 'sanitize-html'; + +@Injectable() +export class CommentsService { + private readonly sanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], + allowedAttributes: { + a: ['href', 'title'], + }, + allowedSchemes: ['http', 'https', 'mailto'], + }; + + async create(dto: CreateCommentDto): Promise { + return this.repo.save({ + content: sanitizeHtml(dto.content, this.sanitizeOptions), + authorId: dto.authorId, + }); + } +} + +// Use validation pipe to strip HTML +import { Transform } from 'class-transformer'; + +export class CreatePostDto { + @IsString() + @MaxLength(1000) + @Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] })) + title: string; + + @IsString() + @Transform(({ value }) => + sanitizeHtml(value, { + allowedTags: ['p', 'br', 'b', 'i', 'a'], + allowedAttributes: { a: ['href'] }, + }), + ) + content: string; +} + +// Set proper Content-Type headers +@Controller('api') +export class ApiController { + @Get('data') + @Header('Content-Type', 'application/json') + async getData(): Promise { + // JSON response - browser won't execute scripts + return this.service.getData(); + } +} + +// Sanitize error messages +@Get(':id') +async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + // UUID validation ensures safe format + throw new NotFoundException('User not found'); + } + return user; +} + +// Use Helmet for CSP headers +import helmet from 'helmet'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https:'], + }, + }, + }), + ); + + await app.listen(3000); +} +``` + +Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) + +--- + +### 4.4 Use Guards for Authentication and Authorization + +**Impact: HIGH** — Enforces access control before handlers execute + +Guards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers. + +**Incorrect (manual auth checks in every handler):** + +```typescript +// Manual auth checks in every handler +@Controller('admin') +export class AdminController { + @Get('users') + async getUsers(@Request() req) { + if (!req.user) { + throw new UnauthorizedException(); + } + if (!req.user.roles.includes('admin')) { + throw new ForbiddenException(); + } + return this.adminService.getUsers(); + } + + @Delete('users/:id') + async deleteUser(@Request() req, @Param('id') id: string) { + if (!req.user) { + throw new UnauthorizedException(); + } + if (!req.user.roles.includes('admin')) { + throw new ForbiddenException(); + } + return this.adminService.deleteUser(id); + } +} +``` + +**Correct (guards with declarative decorators):** + +```typescript +// JWT Auth Guard +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // Check for @Public() decorator + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + const request = context.switchToHttp().getRequest(); + const token = this.extractToken(request); + + if (!token) { + throw new UnauthorizedException('No token provided'); + } + + try { + request.user = await this.jwtService.verifyAsync(token); + return true; + } catch { + throw new UnauthorizedException('Invalid token'); + } + } + + private extractToken(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} + +// Roles Guard +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride('roles', [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) return true; + + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.some((role) => user.roles?.includes(role)); + } +} + +// Decorators +export const Public = () => SetMetadata('isPublic', true); +export const Roles = (...roles: Role[]) => SetMetadata('roles', roles); + +// Register guards globally +@Module({ + providers: [ + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: RolesGuard }, + ], +}) +export class AppModule {} + +// Clean controller +@Controller('admin') +@Roles(Role.Admin) // Applied to all routes +export class AdminController { + @Get('users') + getUsers(): Promise { + return this.adminService.getUsers(); + } + + @Delete('users/:id') + deleteUser(@Param('id') id: string): Promise { + return this.adminService.deleteUser(id); + } + + @Public() // Override: no auth required + @Get('health') + health() { + return { status: 'ok' }; + } +} +``` + +Reference: [NestJS Guards](https://docs.nestjs.com/guards) + +--- + +### 4.5 Validate All Input with DTOs and Pipes + +**Impact: HIGH** — First line of defense against attacks + +Always validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing. + +**Incorrect (trust raw input without validation):** + +```typescript +// Trust raw input without validation +@Controller('users') +export class UsersController { + @Post() + create(@Body() body: any) { + // body could contain anything - SQL injection, XSS, etc. + return this.usersService.create(body); + } + + @Get() + findAll(@Query() query: any) { + // query.limit could be "'; DROP TABLE users; --" + return this.usersService.findAll(query.limit); + } +} + +// DTOs without validation decorators +export class CreateUserDto { + name: string; // No validation + email: string; // Could be "not-an-email" + age: number; // Could be "abc" or -999 +} +``` + +**Correct (validated DTOs with global ValidationPipe):** + +```typescript +// Enable ValidationPipe globally in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strip unknown properties + forbidNonWhitelisted: true, // Throw on unknown properties + transform: true, // Auto-transform to DTO types + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + + await app.listen(3000); +} + +// Create well-validated DTOs +import { + IsString, + IsEmail, + IsInt, + Min, + Max, + IsOptional, + MinLength, + MaxLength, + Matches, + IsNotEmpty, +} from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +export class CreateUserDto { + @IsString() + @IsNotEmpty() + @MinLength(2) + @MaxLength(100) + @Transform(({ value }) => value?.trim()) + name: string; + + @IsEmail() + @Transform(({ value }) => value?.toLowerCase().trim()) + email: string; + + @IsInt() + @Min(0) + @Max(150) + age: number; + + @IsString() + @MinLength(8) + @MaxLength(100) + @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { + message: 'Password must contain uppercase, lowercase, and number', + }) + password: string; +} + +// Query DTO with defaults and transformation +export class FindUsersQueryDto { + @IsOptional() + @IsString() + @MaxLength(100) + search?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit: number = 20; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset: number = 0; +} + +// Param validation +export class UserIdParamDto { + @IsUUID('4') + id: string; +} + +@Controller('users') +export class UsersController { + @Post() + create(@Body() dto: CreateUserDto): Promise { + // dto is guaranteed to be valid + return this.usersService.create(dto); + } + + @Get() + findAll(@Query() query: FindUsersQueryDto): Promise { + // query.limit is a number, query.search is sanitized + return this.usersService.findAll(query); + } + + @Get(':id') + findOne(@Param() params: UserIdParamDto): Promise { + // params.id is a valid UUID + return this.usersService.findById(params.id); + } +} +``` + +Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation) + +--- + +## 5. Performance + +**Section Impact: HIGH** + +### 5.1 Use Async Lifecycle Hooks Correctly + +**Impact: HIGH** — Improper async handling blocks application startup + +NestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately. + +**Incorrect (fire-and-forget async without await):** + +```typescript +// Fire-and-forget async without await +@Injectable() +export class DatabaseService implements OnModuleInit { + onModuleInit() { + // This runs but doesn't block - app starts before DB is ready! + this.connect(); + } + + private async connect() { + await this.pool.connect(); + console.log('Database connected'); + } +} + +// Heavy blocking operations in constructor +@Injectable() +export class ConfigService { + private config: Config; + + constructor() { + // BLOCKS entire module instantiation synchronously + this.config = fs.readFileSync('config.json'); + } +} +``` + +**Correct (return promises from async hooks):** + +```typescript +// Return promise from async hooks +@Injectable() +export class DatabaseService implements OnModuleInit { + private pool: Pool; + + async onModuleInit(): Promise { + // NestJS waits for this to complete before continuing + await this.pool.connect(); + console.log('Database connected'); + } + + async onModuleDestroy(): Promise { + // Clean up resources on shutdown + await this.pool.end(); + console.log('Database disconnected'); + } +} + +// Use onApplicationBootstrap for cross-module dependencies +@Injectable() +export class CacheWarmerService implements OnApplicationBootstrap { + constructor( + private cache: CacheService, + private products: ProductsService, + ) {} + + async onApplicationBootstrap(): Promise { + // All modules are initialized, safe to warm cache + const products = await this.products.findPopular(); + await this.cache.warmup(products); + } +} + +// Heavy init in async hooks, not constructor +@Injectable() +export class ConfigService implements OnModuleInit { + private config: Config; + + constructor() { + // Keep constructor synchronous and fast + } + + async onModuleInit(): Promise { + // Async loading in lifecycle hook + this.config = await this.loadConfig(); + } + + private async loadConfig(): Promise { + const file = await fs.promises.readFile('config.json'); + return JSON.parse(file.toString()); + } + + get(key: string): T { + return this.config[key]; + } +} + +// Enable shutdown hooks in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling + await app.listen(3000); +} +``` + +Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events) + +--- + +### 5.2 Use Lazy Loading for Large Modules + +**Impact: MEDIUM** — Improves startup time for large applications + +NestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs. + +**Incorrect (loading everything eagerly):** + +```typescript +// Load everything eagerly in a large app +@Module({ + imports: [ + UsersModule, + OrdersModule, + PaymentsModule, + ReportsModule, // Heavy, rarely used + AnalyticsModule, // Heavy, rarely used + AdminModule, // Only admins use this + LegacyModule, // Migration module, rarely used + BulkImportModule, // Used once a month + ], +}) +export class AppModule {} + +// All modules initialize at startup, even if never used +// Slow cold starts in serverless +// Memory wasted on unused modules +``` + +**Correct (lazy load rarely-used modules):** + +```typescript +// Use LazyModuleLoader for optional modules +import { LazyModuleLoader } from '@nestjs/core'; + +@Injectable() +export class ReportsService { + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async generateReport(type: string): Promise { + // Load module only when needed + const { ReportsModule } = await import('./reports/reports.module'); + const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule); + + const reportsService = moduleRef.get(ReportsGeneratorService); + return reportsService.generate(type); + } +} + +// Lazy load admin features with caching +@Injectable() +export class AdminService { + private adminModule: ModuleRef | null = null; + + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + private async getAdminModule(): Promise { + if (!this.adminModule) { + const { AdminModule } = await import('./admin/admin.module'); + this.adminModule = await this.lazyModuleLoader.load(() => AdminModule); + } + return this.adminModule; + } + + async runAdminTask(task: string): Promise { + const moduleRef = await this.getAdminModule(); + const taskRunner = moduleRef.get(AdminTaskRunner); + await taskRunner.run(task); + } +} + +// Reusable lazy loader service +@Injectable() +export class ModuleLoaderService { + private loadedModules = new Map(); + + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async load( + key: string, + importFn: () => Promise<{ default: Type } | Type>, + ): Promise { + if (!this.loadedModules.has(key)) { + const module = await importFn(); + const moduleType = 'default' in module ? module.default : module; + const moduleRef = await this.lazyModuleLoader.load(() => moduleType); + this.loadedModules.set(key, moduleRef); + } + return this.loadedModules.get(key)!; + } +} + +// Preload modules in background after startup +@Injectable() +export class ModulePreloader implements OnApplicationBootstrap { + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async onApplicationBootstrap(): Promise { + setTimeout(async () => { + await this.preloadModule(() => import('./reports/reports.module')); + }, 5000); // 5 seconds after startup + } + + private async preloadModule(importFn: () => Promise): Promise { + try { + const module = await importFn(); + const moduleType = module.default || Object.values(module)[0]; + await this.lazyModuleLoader.load(() => moduleType); + } catch (error) { + console.warn('Failed to preload module', error); + } + } +} +``` + +Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules) + +--- + +### 5.3 Optimize Database Queries + +**Impact: HIGH** — Database queries are typically the largest source of latency + +Select only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries. + +**Incorrect (over-fetching data and missing indexes):** + +```typescript +// Select everything when you need few fields +@Injectable() +export class UsersService { + async findAllEmails(): Promise { + const users = await this.repo.find(); + // Fetches ALL columns for ALL users + return users.map((u) => u.email); + } + + async getUserSummary(id: string): Promise { + const user = await this.repo.findOne({ + where: { id }, + relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'], + }); + // Over-fetches massive relation tree + return { name: user.name, postCount: user.posts.length }; + } +} + +// No indexes on frequently queried columns +@Entity() +export class Order { + @Column() + userId: string; // No index - full table scan on every lookup + + @Column() + status: string; // No index - slow status filtering +} +``` + +**Correct (select only needed data with proper indexes):** + +```typescript +// Select only needed columns +@Injectable() +export class UsersService { + async findAllEmails(): Promise { + const users = await this.repo.find({ + select: ['email'], // Only fetch email column + }); + return users.map((u) => u.email); + } + + // Use QueryBuilder for complex selections + async getUserSummary(id: string): Promise { + return this.repo + .createQueryBuilder('user') + .select('user.name', 'name') + .addSelect('COUNT(post.id)', 'postCount') + .leftJoin('user.posts', 'post') + .where('user.id = :id', { id }) + .groupBy('user.id') + .getRawOne(); + } + + // Fetch relations only when needed + async getFullProfile(id: string): Promise { + return this.repo.findOne({ + where: { id }, + relations: ['posts'], // Only immediate relation + select: { + id: true, + name: true, + email: true, + posts: { + id: true, + title: true, + }, + }, + }); + } +} + +// Add indexes on frequently queried columns +@Entity() +@Index(['userId']) +@Index(['status']) +@Index(['createdAt']) +@Index(['userId', 'status']) // Composite index for common query pattern +export class Order { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column() + status: string; + + @CreateDateColumn() + createdAt: Date; +} + +// Always paginate large datasets +@Injectable() +export class OrdersService { + async findAll(page = 1, limit = 20): Promise> { + const [items, total] = await this.repo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + order: { createdAt: 'DESC' }, + }); + + return { + items, + meta: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }; + } +} +``` + +Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder) + +--- + +### 5.4 Use Caching Strategically + +**Impact: HIGH** — Dramatically reduces database load and response times + +Implement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas. + +**Incorrect (no caching or caching everything):** + +```typescript +// No caching for expensive, repeated queries +@Injectable() +export class ProductsService { + async getPopular(): Promise { + // Runs complex aggregation query EVERY request + return this.productsRepo + .createQueryBuilder('p') + .leftJoin('p.orders', 'o') + .select('p.*, COUNT(o.id) as orderCount') + .groupBy('p.id') + .orderBy('orderCount', 'DESC') + .limit(20) + .getMany(); + } +} + +// Cache everything without thought +@Injectable() +export class UsersService { + @CacheKey('users') + @CacheTTL(3600) + @UseInterceptors(CacheInterceptor) + async findAll(): Promise { + // Caching user list for 1 hour is wrong if data changes frequently + return this.usersRepo.find(); + } +} +``` + +**Correct (strategic caching with proper invalidation):** + +```typescript +// Setup caching module +@Module({ + imports: [ + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + stores: [ + new KeyvRedis(config.get('REDIS_URL')), + ], + ttl: 60 * 1000, // Default 60s + }), + }), + ], +}) +export class AppModule {} + +// Manual caching for granular control +@Injectable() +export class ProductsService { + constructor( + @Inject(CACHE_MANAGER) private cache: Cache, + private productsRepo: ProductRepository, + ) {} + + async getPopular(): Promise { + const cacheKey = 'products:popular'; + + // Try cache first + const cached = await this.cache.get(cacheKey); + if (cached) return cached; + + // Cache miss - fetch and cache + const products = await this.fetchPopularProducts(); + await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL + return products; + } + + // Invalidate cache on changes + async updateProduct(id: string, dto: UpdateProductDto): Promise { + const product = await this.productsRepo.save({ id, ...dto }); + await this.cache.del('products:popular'); // Invalidate + return product; + } +} + +// Decorator-based caching with auto-interceptor +@Controller('categories') +@UseInterceptors(CacheInterceptor) +export class CategoriesController { + @Get() + @CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change + findAll(): Promise { + return this.categoriesService.findAll(); + } + + @Get(':id') + @CacheTTL(60 * 1000) // 1 minute + @CacheKey('category') + findOne(@Param('id') id: string): Promise { + return this.categoriesService.findOne(id); + } +} + +// Event-based cache invalidation +@Injectable() +export class CacheInvalidationService { + constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} + + @OnEvent('product.created') + @OnEvent('product.updated') + @OnEvent('product.deleted') + async invalidateProductCaches(event: ProductEvent) { + await Promise.all([ + this.cache.del('products:popular'), + this.cache.del(`product:${event.productId}`), + ]); + } +} +``` + +Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching) + +--- + +## 6. Testing + +**Section Impact: MEDIUM-HIGH** + +### 6.1 Use Supertest for E2E Testing + +**Impact: HIGH** — Validates the full request/response cycle + +End-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss. + +**Incorrect (no proper E2E setup or teardown):** + +```typescript +// Only unit test controllers +describe('UsersController', () => { + it('should return users', async () => { + const service = { findAll: jest.fn().mockResolvedValue([]) }; + const controller = new UsersController(service as any); + + const result = await controller.findAll(); + + expect(result).toEqual([]); + // Doesn't test: routes, guards, pipes, serialization + }); +}); + +// E2E tests without proper setup/teardown +describe('Users API', () => { + it('should create user', async () => { + const app = await NestFactory.create(AppModule); + // No proper initialization + // No cleanup after test + // Hits real database + }); +}); +``` + +**Correct (proper E2E setup with Supertest):** + +```typescript +// Proper E2E test setup +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('UsersController (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + + // Apply same config as production + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('/users (POST)', () => { + it('should create a user', () => { + return request(app.getHttpServer()) + .post('/users') + .send({ name: 'John', email: 'john@test.com' }) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id'); + expect(res.body.name).toBe('John'); + expect(res.body.email).toBe('john@test.com'); + }); + }); + + it('should return 400 for invalid email', () => { + return request(app.getHttpServer()) + .post('/users') + .send({ name: 'John', email: 'invalid-email' }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('email'); + }); + }); + }); + + describe('/users/:id (GET)', () => { + it('should return 404 for non-existent user', () => { + return request(app.getHttpServer()) + .get('/users/non-existent-id') + .expect(404); + }); + }); +}); + +// Testing with authentication +describe('Protected Routes (e2e)', () => { + let app: INestApplication; + let authToken: string; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + await app.init(); + + // Get auth token + const loginResponse = await request(app.getHttpServer()) + .post('/auth/login') + .send({ email: 'test@test.com', password: 'password' }); + + authToken = loginResponse.body.accessToken; + }); + + it('should return 401 without token', () => { + return request(app.getHttpServer()) + .get('/users/me') + .expect(401); + }); + + it('should return user profile with valid token', () => { + return request(app.getHttpServer()) + .get('/users/me') + .set('Authorization', `Bearer ${authToken}`) + .expect(200) + .expect((res) => { + expect(res.body.email).toBe('test@test.com'); + }); + }); +}); + +// Database isolation for E2E tests +describe('Orders API (e2e)', () => { + let app: INestApplication; + let dataSource: DataSource; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: '.env.test', // Test database config + }), + AppModule, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + dataSource = moduleFixture.get(DataSource); + await app.init(); + }); + + beforeEach(async () => { + // Clean database between tests + await dataSource.synchronize(true); + }); + + afterAll(async () => { + await dataSource.destroy(); + await app.close(); + }); +}); +``` + +Reference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing) + +--- + +### 6.2 Mock External Services in Tests + +**Impact: HIGH** — Ensures fast, reliable, deterministic tests + +Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors. + +**Incorrect (calling real APIs and databases):** + +```typescript +// Call real APIs in tests +describe('PaymentService', () => { + it('should process payment', async () => { + const service = new PaymentService(new StripeClient(realApiKey)); + // Hits real Stripe API! + const result = await service.charge('tok_visa', 1000); + // Slow, costs money, flaky + }); +}); + +// Use real database +describe('UsersService', () => { + beforeEach(async () => { + await connection.query('DELETE FROM users'); // Modifies real DB + }); + + it('should create user', async () => { + const user = await service.create({ email: 'test@test.com' }); + // Side effects on shared database + }); +}); + +// Incomplete mocks +const mockHttpService = { + get: jest.fn().mockResolvedValue({ data: {} }), + // Missing error scenarios, missing other methods +}; +``` + +**Correct (mock all external dependencies):** + +```typescript +// Mock HTTP service properly +describe('WeatherService', () => { + let service: WeatherService; + let httpService: jest.Mocked; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + WeatherService, + { + provide: HttpService, + useValue: { + get: jest.fn(), + post: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(WeatherService); + httpService = module.get(HttpService); + }); + + it('should return weather data', async () => { + const mockResponse = { + data: { temperature: 72, humidity: 45 }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + + httpService.get.mockReturnValue(of(mockResponse)); + + const result = await service.getWeather('NYC'); + + expect(result).toEqual({ temperature: 72, humidity: 45 }); + }); + + it('should handle API timeout', async () => { + httpService.get.mockReturnValue( + throwError(() => new Error('ETIMEDOUT')), + ); + + await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable'); + }); + + it('should handle rate limiting', async () => { + httpService.get.mockReturnValue( + throwError(() => ({ + response: { status: 429, data: { message: 'Rate limited' } }, + })), + ); + + await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException); + }); +}); + +// Mock repository instead of database +describe('UsersService', () => { + let service: UsersService; + let repo: jest.Mocked>; + + beforeEach(async () => { + const mockRepo = { + find: jest.fn(), + findOne: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + createQueryBuilder: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + UsersService, + { provide: getRepositoryToken(User), useValue: mockRepo }, + ], + }).compile(); + + service = module.get(UsersService); + repo = module.get(getRepositoryToken(User)); + }); + + it('should find user by id', async () => { + const mockUser = { id: '1', name: 'John', email: 'john@test.com' }; + repo.findOne.mockResolvedValue(mockUser); + + const result = await service.findById('1'); + + expect(result).toEqual(mockUser); + expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + }); +}); + +// Create mock factory for complex SDKs +function createMockStripe(): jest.Mocked { + return { + paymentIntents: { + create: jest.fn(), + retrieve: jest.fn(), + confirm: jest.fn(), + cancel: jest.fn(), + }, + customers: { + create: jest.fn(), + retrieve: jest.fn(), + }, + } as any; +} + +// Mock time for time-dependent tests +describe('TokenService', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-15')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should expire token after 1 hour', async () => { + const token = await service.createToken(); + + // Fast-forward time + jest.advanceTimersByTime(61 * 60 * 1000); + + expect(await service.isValid(token)).toBe(false); + }); +}); +``` + +Reference: [Jest Mocking](https://jestjs.io/docs/mock-functions) + +--- + +### 6.3 Use Testing Module for Unit Tests + +**Impact: HIGH** — Enables proper isolated testing with mocked dependencies + +Use `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation. + +**Incorrect (manual instantiation bypassing DI):** + +```typescript +// Instantiate services manually without DI +describe('UsersService', () => { + it('should create user', async () => { + // Manual instantiation bypasses DI + const repo = new UserRepository(); // Real repo! + const service = new UsersService(repo); + + const user = await service.create({ name: 'Test' }); + // This hits the real database! + }); +}); + +// Test implementation details +describe('UsersController', () => { + it('should call service', async () => { + const service = { create: jest.fn() }; + const controller = new UsersController(service as any); + + await controller.create({ name: 'Test' }); + + expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior + }); +}); +``` + +**Correct (use Test.createTestingModule with mocked dependencies):** + +```typescript +// Use Test.createTestingModule for proper DI +import { Test, TestingModule } from '@nestjs/testing'; + +describe('UsersService', () => { + let service: UsersService; + let repo: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UsersService, + { + provide: UserRepository, + useValue: { + save: jest.fn(), + findOne: jest.fn(), + find: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(UsersService); + repo = module.get(UserRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should save and return user', async () => { + const dto = { name: 'John', email: 'john@test.com' }; + const expectedUser = { id: '1', ...dto }; + + repo.save.mockResolvedValue(expectedUser); + + const result = await service.create(dto); + + expect(result).toEqual(expectedUser); + expect(repo.save).toHaveBeenCalledWith(dto); + }); + + it('should throw on duplicate email', async () => { + repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' }); + + await expect( + service.create({ name: 'Test', email: 'test@test.com' }), + ).rejects.toThrow(ConflictException); + }); + }); + + describe('findById', () => { + it('should return user when found', async () => { + const user = { id: '1', name: 'John' }; + repo.findOne.mockResolvedValue(user); + + const result = await service.findById('1'); + + expect(result).toEqual(user); + }); + + it('should throw NotFoundException when not found', async () => { + repo.findOne.mockResolvedValue(null); + + await expect(service.findById('999')).rejects.toThrow(NotFoundException); + }); + }); +}); + +// Testing guards and interceptors +describe('RolesGuard', () => { + let guard: RolesGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [RolesGuard, Reflector], + }).compile(); + + guard = module.get(RolesGuard); + reflector = module.get(Reflector); + }); + + it('should allow when no roles required', () => { + const context = createMockExecutionContext({ user: { roles: [] } }); + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined); + + expect(guard.canActivate(context)).toBe(true); + }); + + it('should allow admin for admin-only route', () => { + const context = createMockExecutionContext({ user: { roles: ['admin'] } }); + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); + + expect(guard.canActivate(context)).toBe(true); + }); +}); + +function createMockExecutionContext(request: Partial): ExecutionContext { + return { + switchToHttp: () => ({ + getRequest: () => request, + }), + getHandler: () => jest.fn(), + getClass: () => jest.fn(), + } as ExecutionContext; +} +``` + +Reference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing) + +--- + +## 7. Database & ORM + +**Section Impact: MEDIUM-HIGH** + +### 7.1 Avoid N+1 Query Problems + +**Impact: HIGH** — N+1 queries are one of the most common performance killers + +N+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently. + +**Incorrect (lazy loading in loops causes N+1):** + +```typescript +// Lazy loading in loops causes N+1 +@Injectable() +export class OrdersService { + async getOrdersWithItems(userId: string): Promise { + const orders = await this.orderRepo.find({ where: { userId } }); + // 1 query for orders + + for (const order of orders) { + // N additional queries - one per order! + order.items = await this.itemRepo.find({ where: { orderId: order.id } }); + } + + return orders; + } +} + +// Accessing lazy relations without loading +@Controller('users') +export class UsersController { + @Get() + async findAll(): Promise { + const users = await this.userRepo.find(); + // If User.posts is lazy-loaded, serializing triggers N queries + return users; // Each user.posts access = 1 query + } +} +``` + +**Correct (use relations for eager loading):** + +```typescript +// Use relations option for eager loading +@Injectable() +export class OrdersService { + async getOrdersWithItems(userId: string): Promise { + // Single query with JOIN + return this.orderRepo.find({ + where: { userId }, + relations: ['items', 'items.product'], + }); + } +} + +// Use QueryBuilder for complex joins +@Injectable() +export class UsersService { + async getUsersWithPostCounts(): Promise { + return this.userRepo + .createQueryBuilder('user') + .leftJoin('user.posts', 'post') + .select('user.id', 'id') + .addSelect('user.name', 'name') + .addSelect('COUNT(post.id)', 'postCount') + .groupBy('user.id') + .getRawMany(); + } + + async getActiveUsersWithPosts(): Promise { + return this.userRepo + .createQueryBuilder('user') + .leftJoinAndSelect('user.posts', 'post') + .leftJoinAndSelect('post.comments', 'comment') + .where('user.isActive = :active', { active: true }) + .andWhere('post.status = :status', { status: 'published' }) + .getMany(); + } +} + +// Use find options for specific fields +async getOrderSummaries(userId: string): Promise { + return this.orderRepo.find({ + where: { userId }, + relations: ['items'], + select: { + id: true, + total: true, + status: true, + items: { + id: true, + quantity: true, + price: true, + }, + }, + }); +} + +// Use DataLoader for GraphQL to batch and cache queries +import DataLoader from 'dataloader'; + +@Injectable({ scope: Scope.REQUEST }) +export class PostsLoader { + constructor(private postsService: PostsService) {} + + readonly batchPosts = new DataLoader(async (userIds) => { + // Single query for all users' posts + const posts = await this.postsService.findByUserIds([...userIds]); + + // Group by userId + const postsMap = new Map(); + for (const post of posts) { + const userPosts = postsMap.get(post.userId) || []; + userPosts.push(post); + postsMap.set(post.userId, userPosts); + } + + // Return in same order as input + return userIds.map((id) => postsMap.get(id) || []); + }); +} + +// In resolver +@ResolveField() +async posts(@Parent() user: User): Promise { + // DataLoader batches multiple calls into single query + return this.postsLoader.batchPosts.load(user.id); +} + +// Enable query logging in development to detect N+1 +TypeOrmModule.forRoot({ + logging: ['query', 'error'], + logger: 'advanced-console', +}); +``` + +Reference: [TypeORM Relations](https://typeorm.io/relations) + +--- + +### 7.2 Use Database Migrations + +**Impact: HIGH** — Enables safe, repeatable database schema changes + +Never use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments. + +**Incorrect (using synchronize or manual SQL):** + +```typescript +// Use synchronize in production +TypeOrmModule.forRoot({ + type: 'postgres', + synchronize: true, // DANGEROUS in production! + // Can drop columns, tables, or data +}); + +// Manual SQL in production +@Injectable() +export class DatabaseService { + async addColumn(): Promise { + await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT'); + // No version control, no rollback, inconsistent across envs + } +} + +// Modify entities without migration +@Entity() +export class User { + @Column() + email: string; + + @Column() // Added without migration + newField: string; // Will crash in production if synchronize is false +} +``` + +**Correct (use migrations for all schema changes):** + +```typescript +// Configure TypeORM for migrations +// data-source.ts +export const dataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: ['dist/**/*.entity.js'], + migrations: ['dist/migrations/*.js'], + synchronize: false, // Always false in production + migrationsRun: true, // Run migrations on startup +}); + +// app.module.ts +TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('DB_HOST'), + synchronize: config.get('NODE_ENV') === 'development', // Only in dev + migrations: ['dist/migrations/*.js'], + migrationsRun: true, + }), +}); + +// migrations/1705312800000-AddUserAge.ts +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserAge1705312800000 implements MigrationInterface { + name = 'AddUserAge1705312800000'; + + public async up(queryRunner: QueryRunner): Promise { + // Add column with default to handle existing rows + await queryRunner.query(` + ALTER TABLE "users" ADD "age" integer DEFAULT 0 + `); + + // Add index for frequently queried columns + await queryRunner.query(` + CREATE INDEX "IDX_users_age" ON "users" ("age") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Always implement down for rollback + await queryRunner.query(`DROP INDEX "IDX_users_age"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "age"`); + } +} + +// Safe column rename (two-step) +export class RenameNameToFullName1705312900000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Step 1: Add new column + await queryRunner.query(` + ALTER TABLE "users" ADD "full_name" varchar(255) + `); + + // Step 2: Copy data + await queryRunner.query(` + UPDATE "users" SET "full_name" = "name" + `); + + // Step 3: Add NOT NULL constraint + await queryRunner.query(` + ALTER TABLE "users" ALTER COLUMN "full_name" SET NOT NULL + `); + + // Step 4: Drop old column (after verifying app works) + await queryRunner.query(` + ALTER TABLE "users" DROP COLUMN "name" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "name" varchar(255)`); + await queryRunner.query(`UPDATE "users" SET "name" = "full_name"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "full_name"`); + } +} +``` + +Reference: [TypeORM Migrations](https://typeorm.io/migrations) + +--- + +### 7.3 Use Transactions for Multi-Step Operations + +**Impact: HIGH** — Ensures data consistency in multi-step operations + +When multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios. + +**Incorrect (multiple saves without transaction):** + +```typescript +// Multiple saves without transaction +@Injectable() +export class OrdersService { + async createOrder(userId: string, items: OrderItem[]): Promise { + // If any step fails, data is inconsistent + const order = await this.orderRepo.save({ userId, status: 'pending' }); + + for (const item of items) { + await this.orderItemRepo.save({ orderId: order.id, ...item }); + await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity); + } + + await this.paymentService.charge(order.id); + // If payment fails, order and inventory are already modified! + + return order; + } +} +``` + +**Correct (use DataSource.transaction for automatic rollback):** + +```typescript +// Use DataSource.transaction() for automatic rollback +@Injectable() +export class OrdersService { + constructor(private dataSource: DataSource) {} + + async createOrder(userId: string, items: OrderItem[]): Promise { + return this.dataSource.transaction(async (manager) => { + // All operations use the same transactional manager + const order = await manager.save(Order, { userId, status: 'pending' }); + + for (const item of items) { + await manager.save(OrderItem, { orderId: order.id, ...item }); + await manager.decrement( + Inventory, + { productId: item.productId }, + 'stock', + item.quantity, + ); + } + + // If this throws, everything rolls back + await this.paymentService.chargeWithManager(manager, order.id); + + return order; + }); + } +} + +// QueryRunner for manual transaction control +@Injectable() +export class TransferService { + constructor(private dataSource: DataSource) {} + + async transfer(fromId: string, toId: string, amount: number): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Debit source account + await queryRunner.manager.decrement( + Account, + { id: fromId }, + 'balance', + amount, + ); + + // Verify sufficient funds + const source = await queryRunner.manager.findOne(Account, { + where: { id: fromId }, + }); + if (source.balance < 0) { + throw new BadRequestException('Insufficient funds'); + } + + // Credit destination account + await queryRunner.manager.increment( + Account, + { id: toId }, + 'balance', + amount, + ); + + // Log the transaction + await queryRunner.manager.save(TransactionLog, { + fromId, + toId, + amount, + timestamp: new Date(), + }); + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } +} + +// Repository method with transaction support +@Injectable() +export class UsersRepository { + constructor( + @InjectRepository(User) private repo: Repository, + private dataSource: DataSource, + ) {} + + async createWithProfile( + userData: CreateUserDto, + profileData: CreateProfileDto, + ): Promise { + return this.dataSource.transaction(async (manager) => { + const user = await manager.save(User, userData); + await manager.save(Profile, { ...profileData, userId: user.id }); + return user; + }); + } +} +``` + +Reference: [TypeORM Transactions](https://typeorm.io/transactions) + +--- + +## 8. API Design + +**Section Impact: MEDIUM** + +### 8.1 Use DTOs and Serialization for API Responses + +**Impact: MEDIUM** — Response DTOs prevent accidental data exposure and ensure consistency + +Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract. + +**Incorrect (returning entities directly or manual spreading):** + +```typescript +// Return entities directly +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + // Returns: { id, email, passwordHash, ssn, internalNotes, ... } + // Exposes sensitive data! + } +} + +// Manual object spreading (error-prone) +@Get(':id') +async findOne(@Param('id') id: string) { + const user = await this.usersService.findById(id); + return { + id: user.id, + email: user.email, + name: user.name, + // Easy to forget to exclude sensitive fields + // Hard to maintain across endpoints + }; +} +``` + +**Correct (use class-transformer with @Exclude and response DTOs):** + +```typescript +// Enable class-transformer globally +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + await app.listen(3000); +} + +// Entity with serialization control +@Entity() +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + email: string; + + @Column() + name: string; + + @Column() + @Exclude() // Never include in responses + passwordHash: string; + + @Column({ nullable: true }) + @Exclude() + ssn: string; + + @Column({ default: false }) + @Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests + isAdmin: boolean; + + @CreateDateColumn() + createdAt: Date; + + @Column() + @Exclude() + internalNotes: string; +} + +// Now returning entity is safe +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + // Returns: { id, email, name, createdAt } + // Sensitive fields excluded automatically + } +} + +// For different response shapes, use explicit DTOs +export class UserResponseDto { + @Expose() + id: string; + + @Expose() + email: string; + + @Expose() + name: string; + + @Expose() + @Transform(({ obj }) => obj.posts?.length || 0) + postCount: number; + + constructor(partial: Partial) { + Object.assign(this, partial); + } +} + +export class UserDetailResponseDto extends UserResponseDto { + @Expose() + createdAt: Date; + + @Expose() + @Type(() => PostResponseDto) + posts: PostResponseDto[]; +} + +// Controller with explicit DTOs +@Controller('users') +export class UsersController { + @Get() + @SerializeOptions({ type: UserResponseDto }) + async findAll(): Promise { + const users = await this.usersService.findAll(); + return users.map(u => plainToInstance(UserResponseDto, u)); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findByIdWithPosts(id); + return plainToInstance(UserDetailResponseDto, user, { + excludeExtraneousValues: true, + }); + } +} + +// Groups for conditional serialization +export class UserDto { + @Expose() + id: string; + + @Expose() + name: string; + + @Expose({ groups: ['admin'] }) + email: string; + + @Expose({ groups: ['admin'] }) + createdAt: Date; + + @Expose({ groups: ['admin', 'owner'] }) + settings: UserSettings; +} + +@Controller('users') +export class UsersController { + @Get() + @SerializeOptions({ groups: ['public'] }) + async findAllPublic(): Promise { + // Returns: { id, name } + } + + @Get('admin') + @UseGuards(AdminGuard) + @SerializeOptions({ groups: ['admin'] }) + async findAllAdmin(): Promise { + // Returns: { id, name, email, createdAt } + } + + @Get('me') + @SerializeOptions({ groups: ['owner'] }) + async getProfile(@CurrentUser() user: User): Promise { + // Returns: { id, name, settings } + } +} +``` + +Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization) + +--- + +### 8.2 Use Interceptors for Cross-Cutting Concerns + +**Impact: MEDIUM-HIGH** — Interceptors provide clean separation for cross-cutting logic + +Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams. + +**Incorrect (logging and transformation in every method):** + +```typescript +// Logging in every controller method +@Controller('users') +export class UsersController { + @Get() + async findAll(): Promise { + const start = Date.now(); + this.logger.log('findAll called'); + + const users = await this.usersService.findAll(); + + this.logger.log(`findAll completed in ${Date.now() - start}ms`); + return users; + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const start = Date.now(); + this.logger.log(`findOne called with id: ${id}`); + + const user = await this.usersService.findOne(id); + + this.logger.log(`findOne completed in ${Date.now() - start}ms`); + return user; + } + // Repeated in every method! +} + +// Manual response wrapping +@Get() +async findAll(): Promise<{ data: User[]; meta: Meta }> { + const users = await this.usersService.findAll(); + return { + data: users, + meta: { timestamp: new Date(), count: users.length }, + }; +} +``` + +**Correct (use interceptors for cross-cutting concerns):** + +```typescript +// Logging interceptor +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private readonly logger = new Logger('HTTP'); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body } = request; + const now = Date.now(); + + return next.handle().pipe( + tap({ + next: (data) => { + const response = context.switchToHttp().getResponse(); + this.logger.log( + `${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`, + ); + }, + error: (error) => { + this.logger.error( + `${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`, + error.stack, + ); + }, + }), + ); + } +} + +// Response transformation interceptor +@Injectable() +export class TransformInterceptor implements NestInterceptor> { + intercept(context: ExecutionContext, next: CallHandler): Observable> { + return next.handle().pipe( + map((data) => ({ + data, + meta: { + timestamp: new Date().toISOString(), + path: context.switchToHttp().getRequest().url, + }, + })), + ); + } +} + +// Timeout interceptor +@Injectable() +export class TimeoutInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + timeout(5000), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException('Request timed out'); + } + throw err; + }), + ); + } +} + +// Apply globally or per-controller +@Module({ + providers: [ + { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, + { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, + ], +}) +export class AppModule {} + +// Or per-controller +@Controller('users') +@UseInterceptors(LoggingInterceptor) +export class UsersController { + @Get() + async findAll(): Promise { + // Clean business logic only + return this.usersService.findAll(); + } +} + +// Custom cache interceptor with TTL +@Injectable() +export class HttpCacheInterceptor implements NestInterceptor { + constructor( + private cacheManager: Cache, + private reflector: Reflector, + ) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request = context.switchToHttp().getRequest(); + + // Only cache GET requests + if (request.method !== 'GET') { + return next.handle(); + } + + const cacheKey = this.generateKey(request); + const ttl = this.reflector.get('cacheTTL', context.getHandler()) || 300; + + const cached = await this.cacheManager.get(cacheKey); + if (cached) { + return of(cached); + } + + return next.handle().pipe( + tap((response) => { + this.cacheManager.set(cacheKey, response, ttl); + }), + ); + } + + private generateKey(request: Request): string { + return `cache:${request.url}:${JSON.stringify(request.query)}`; + } +} + +// Usage with custom TTL +@Get() +@SetMetadata('cacheTTL', 600) +@UseInterceptors(HttpCacheInterceptor) +async findAll(): Promise { + return this.usersService.findAll(); +} + +// Error mapping interceptor +@Injectable() +export class ErrorMappingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + catchError((error) => { + if (error instanceof EntityNotFoundError) { + throw new NotFoundException(error.message); + } + if (error instanceof QueryFailedError) { + if (error.message.includes('duplicate')) { + throw new ConflictException('Resource already exists'); + } + } + throw error; + }), + ); + } +} +``` + +Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors) + +--- + +### 8.3 Use Pipes for Input Transformation + +**Impact: MEDIUM** — Pipes ensure clean, validated data reaches your handlers + +Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers. + +**Incorrect (manual type parsing in handlers):** + +```typescript +// Manual type parsing in handlers +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + // Manual validation in every handler + const uuid = id.trim(); + if (!isUUID(uuid)) { + throw new BadRequestException('Invalid UUID'); + } + return this.usersService.findOne(uuid); + } + + @Get() + async findAll( + @Query('page') page: string, + @Query('limit') limit: string, + ): Promise { + // Manual parsing and defaults + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + return this.usersService.findAll(pageNum, limitNum); + } +} + +// Type coercion without validation +@Get() +async search(@Query('price') price: string): Promise { + const priceNum = +price; // NaN if invalid, no error + return this.productsService.findByPrice(priceNum); +} +``` + +**Correct (use built-in and custom pipes):** + +```typescript +// Use built-in pipes for common transformations +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { + // id is guaranteed to be a valid UUID + return this.usersService.findOne(id); + } + + @Get() + async findAll( + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, + ): Promise { + // Automatic defaults and type conversion + return this.usersService.findAll(page, limit); + } + + @Get('by-status/:status') + async findByStatus( + @Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus, + ): Promise { + return this.usersService.findByStatus(status); + } +} + +// Custom pipe for business logic +@Injectable() +export class ParseDatePipe implements PipeTransform { + transform(value: string): Date { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new BadRequestException('Invalid date format'); + } + return date; + } +} + +@Get('reports') +async getReports( + @Query('from', ParseDatePipe) from: Date, + @Query('to', ParseDatePipe) to: Date, +): Promise { + return this.reportsService.findBetween(from, to); +} + +// Custom transformation pipes +@Injectable() +export class NormalizeEmailPipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + return value.trim().toLowerCase(); + } +} + +// Parse comma-separated values +@Injectable() +export class ParseArrayPipe implements PipeTransform { + transform(value: string): string[] { + if (!value) return []; + return value.split(',').map((v) => v.trim()).filter(Boolean); + } +} + +@Get('products') +async findProducts( + @Query('ids', ParseArrayPipe) ids: string[], + @Query('email', NormalizeEmailPipe) email: string, +): Promise { + // ids is already an array, email is normalized + return this.productsService.findByIds(ids); +} + +// Sanitize HTML input +@Injectable() +export class SanitizeHtmlPipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + return sanitizeHtml(value, { allowedTags: [] }); + } +} + +// Global validation pipe with transformation +app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strip non-DTO properties + transform: true, // Auto-transform to DTO types + transformOptions: { + enableImplicitConversion: true, // Convert query strings to numbers + }, + forbidNonWhitelisted: true, // Throw on extra properties + }), +); + +// DTO with transformation decorators +export class FindProductsDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 10; + + @IsOptional() + @Transform(({ value }) => value?.toLowerCase()) + @IsString() + search?: string; + + @IsOptional() + @Transform(({ value }) => value?.split(',')) + @IsArray() + @IsString({ each: true }) + categories?: string[]; +} + +@Get() +async findAll(@Query() dto: FindProductsDto): Promise { + // dto is already transformed and validated + return this.productsService.findAll(dto); +} + +// Pipe error customization +@Injectable() +export class CustomParseIntPipe extends ParseIntPipe { + constructor() { + super({ + exceptionFactory: (error) => + new BadRequestException(`${error} must be a valid integer`), + }); + } +} + +// Or use options on built-in pipes +@Get(':id') +async findOne( + @Param( + 'id', + new ParseIntPipe({ + errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, + exceptionFactory: () => new NotAcceptableException('ID must be numeric'), + }), + ) + id: number, +): Promise { + return this.itemsService.findOne(id); +} +``` + +Reference: [NestJS Pipes](https://docs.nestjs.com/pipes) + +--- + +### 8.4 Use API Versioning for Breaking Changes + +**Impact: MEDIUM** — Versioning allows you to evolve APIs without breaking existing clients + +Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints. + +**Incorrect (breaking changes without versioning):** + +```typescript +// Breaking changes without versioning +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + // Original response: { id, name, email } + // Later changed to: { id, firstName, lastName, emailAddress } + // Old clients break! + return this.usersService.findOne(id); + } +} + +// Manual versioning in routes +@Controller('v1/users') +export class UsersV1Controller {} + +@Controller('v2/users') +export class UsersV2Controller {} +// Inconsistent, error-prone, hard to maintain +``` + +**Correct (use NestJS built-in versioning):** + +```typescript +// Enable versioning in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // URI versioning: /v1/users, /v2/users + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: '1', + }); + + // Or header versioning: X-API-Version: 1 + app.enableVersioning({ + type: VersioningType.HEADER, + header: 'X-API-Version', + defaultVersion: '1', + }); + + // Or media type: Accept: application/json;v=1 + app.enableVersioning({ + type: VersioningType.MEDIA_TYPE, + key: 'v=', + defaultVersion: '1', + }); + + await app.listen(3000); +} + +// Version-specific controllers +@Controller('users') +@Version('1') +export class UsersV1Controller { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findOne(id); + // V1 response format + return { + id: user.id, + name: user.name, + email: user.email, + }; + } +} + +@Controller('users') +@Version('2') +export class UsersV2Controller { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findOne(id); + // V2 response format with breaking changes + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + emailAddress: user.email, + createdAt: user.createdAt, + }; + } +} + +// Per-route versioning - different versions for different routes +@Controller('users') +export class UsersController { + @Get() + @Version('1') + findAllV1(): Promise { + return this.usersService.findAllV1(); + } + + @Get() + @Version('2') + findAllV2(): Promise { + return this.usersService.findAllV2(); + } + + @Get(':id') + @Version(['1', '2']) // Same handler for multiple versions + findOne(@Param('id') id: string): Promise { + return this.usersService.findOne(id); + } + + @Post() + @Version(VERSION_NEUTRAL) // Available in all versions + create(@Body() dto: CreateUserDto): Promise { + return this.usersService.create(dto); + } +} + +// Shared service with version-specific logic +@Injectable() +export class UsersService { + async findOne(id: string, version: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + + if (version === '1') { + return this.toV1Response(user); + } + return this.toV2Response(user); + } + + private toV1Response(user: User): UserV1Response { + return { + id: user.id, + name: `${user.firstName} ${user.lastName}`, + email: user.email, + }; + } + + private toV2Response(user: User): UserV2Response { + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + emailAddress: user.email, + createdAt: user.createdAt, + }; + } +} + +// Controller extracts version +@Controller('users') +export class UsersController { + @Get(':id') + async findOne( + @Param('id') id: string, + @Headers('X-API-Version') version: string = '1', + ): Promise { + return this.usersService.findOne(id, version); + } +} + +// Deprecation strategy - mark old versions as deprecated +@Controller('users') +@Version('1') +@UseInterceptors(DeprecationInterceptor) +export class UsersV1Controller { + // All V1 routes will include deprecation warning +} + +@Injectable() +export class DeprecationInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const response = context.switchToHttp().getResponse(); + response.setHeader('Deprecation', 'true'); + response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT'); + response.setHeader('Link', '; rel="successor-version"'); + + return next.handle(); + } +} +``` + +Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning) + +--- + +## 9. Microservices + +**Section Impact: MEDIUM** + +### 9.1 Implement Health Checks for Microservices + +**Impact: MEDIUM-HIGH** — Health checks enable orchestrators to manage service lifecycle + +Implement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly. + +**Incorrect (simple ping that doesn't check dependencies):** + +```typescript +// Simple ping that doesn't check dependencies +@Controller('health') +export class HealthController { + @Get() + check(): string { + return 'OK'; // Service might be unhealthy but returns OK + } +} + +// Health check that blocks on slow dependencies +@Controller('health') +export class HealthController { + @Get() + async check(): Promise { + // If database is slow, health check times out + await this.userRepo.findOne({ where: { id: '1' } }); + await this.redis.ping(); + await this.externalApi.healthCheck(); + return 'OK'; + } +} +``` + +**Correct (use @nestjs/terminus for comprehensive health checks):** + +```typescript +// Use @nestjs/terminus for comprehensive health checks +import { + HealthCheckService, + HttpHealthIndicator, + TypeOrmHealthIndicator, + HealthCheck, + DiskHealthIndicator, + MemoryHealthIndicator, +} from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + constructor( + private health: HealthCheckService, + private http: HttpHealthIndicator, + private db: TypeOrmHealthIndicator, + private disk: DiskHealthIndicator, + private memory: MemoryHealthIndicator, + ) {} + + // Liveness probe - is the service alive? + @Get('live') + @HealthCheck() + liveness() { + return this.health.check([ + // Basic checks only + () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB + ]); + } + + // Readiness probe - can the service handle traffic? + @Get('ready') + @HealthCheck() + readiness() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => + this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }), + () => + this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }), + ]); + } + + // Deep health check for debugging + @Get('deep') + @HealthCheck() + deepCheck() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), + () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024), + () => + this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }), + () => + this.http.pingCheck('external-api', 'https://api.example.com/health'), + ]); + } +} + +// Custom indicator for business-specific health +@Injectable() +export class QueueHealthIndicator extends HealthIndicator { + constructor(private queueService: QueueService) { + super(); + } + + async isHealthy(key: string): Promise { + const queueStats = await this.queueService.getStats(); + + const isHealthy = queueStats.failedCount < 100; + const result = this.getStatus(key, isHealthy, { + waiting: queueStats.waitingCount, + active: queueStats.activeCount, + failed: queueStats.failedCount, + }); + + if (!isHealthy) { + throw new HealthCheckError('Queue unhealthy', result); + } + + return result; + } +} + +// Redis health indicator +@Injectable() +export class RedisHealthIndicator extends HealthIndicator { + constructor(@InjectRedis() private redis: Redis) { + super(); + } + + async isHealthy(key: string): Promise { + try { + const pong = await this.redis.ping(); + return this.getStatus(key, pong === 'PONG'); + } catch (error) { + throw new HealthCheckError('Redis check failed', this.getStatus(key, false)); + } + } +} + +// Use custom indicators +@Get('ready') +@HealthCheck() +readiness() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.redis.isHealthy('redis'), + () => this.queue.isHealthy('job-queue'), + ]); +} + +// Graceful shutdown handling +@Injectable() +export class GracefulShutdownService implements OnApplicationShutdown { + private isShuttingDown = false; + + isShutdown(): boolean { + return this.isShuttingDown; + } + + async onApplicationShutdown(signal: string): Promise { + this.isShuttingDown = true; + console.log(`Shutting down on ${signal}`); + + // Wait for in-flight requests + await new Promise((resolve) => setTimeout(resolve, 5000)); + } +} + +// Health check respects shutdown state +@Get('ready') +@HealthCheck() +readiness() { + if (this.shutdownService.isShutdown()) { + throw new ServiceUnavailableException('Shutting down'); + } + + return this.health.check([ + () => this.db.pingCheck('database'), + ]); +} +``` + +### Kubernetes Configuration + +```yaml +# Kubernetes deployment with probes +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-service +spec: + template: + spec: + containers: + - name: api + image: api-service:latest + ports: + - containerPort: 3000 + livenessProbe: + httpGet: + path: /health/live + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health/ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health/live + port: 3000 + initialDelaySeconds: 0 + periodSeconds: 5 + failureThreshold: 30 +``` + +Reference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus) + +--- + +### 9.2 Use Message and Event Patterns Correctly + +**Impact: MEDIUM** — Proper patterns ensure reliable microservice communication + +NestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs. + +**Incorrect (using wrong pattern for use case):** + +```typescript +// Use @MessagePattern for fire-and-forget +@Controller() +export class NotificationsController { + @MessagePattern('user.created') + async handleUserCreated(data: UserCreatedEvent) { + // This WAITS for response, blocking the sender + await this.emailService.sendWelcome(data.email); + // If email fails, sender gets an error (coupling!) + } +} + +// Use @EventPattern expecting a response +@Controller() +export class OrdersController { + @EventPattern('inventory.check') + async checkInventory(data: CheckInventoryDto) { + const available = await this.inventory.check(data); + return available; // This return value is IGNORED with @EventPattern! + } +} + +// Tight coupling in client +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Blocks until notification service responds + await this.client.send('user.created', user).toPromise(); + // If notification service is down, user creation fails! + + return user; + } +} +``` + +**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):** + +```typescript +// MessagePattern: Request-Response (when you NEED a response) +@Controller() +export class InventoryController { + @MessagePattern({ cmd: 'check_inventory' }) + async checkInventory(data: CheckInventoryDto): Promise { + const result = await this.inventoryService.check(data.productId, data.quantity); + return result; // Response sent back to caller + } +} + +// Client expects response +@Injectable() +export class OrdersService { + async createOrder(dto: CreateOrderDto): Promise { + // Check inventory - we NEED this response to proceed + const inventory = await firstValueFrom( + this.inventoryClient.send( + { cmd: 'check_inventory' }, + { productId: dto.productId, quantity: dto.quantity }, + ), + ); + + if (!inventory.available) { + throw new BadRequestException('Insufficient inventory'); + } + + return this.repo.save(dto); + } +} + +// EventPattern: Fire-and-Forget (for notifications, side effects) +@Controller() +export class NotificationsController { + @EventPattern('user.created') + async handleUserCreated(data: UserCreatedEvent): Promise { + // No return value needed - just process the event + await this.emailService.sendWelcome(data.email); + await this.analyticsService.track('user_signup', data); + // If this fails, it doesn't affect the sender + } +} + +// Client emits event without waiting +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Fire and forget - doesn't block, doesn't wait + this.eventClient.emit('user.created', { + userId: user.id, + email: user.email, + timestamp: new Date(), + }); + + return user; // User creation succeeds regardless of event handling + } +} + +// Hybrid pattern for critical events +@Injectable() +export class OrdersService { + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Critical: inventory reservation (use MessagePattern) + const reserved = await firstValueFrom( + this.inventoryClient.send({ cmd: 'reserve_inventory' }, { + orderId: order.id, + items: dto.items, + }), + ); + + if (!reserved.success) { + await this.repo.delete(order.id); + throw new BadRequestException('Could not reserve inventory'); + } + + // Non-critical: notifications (use EventPattern) + this.eventClient.emit('order.created', { + orderId: order.id, + userId: dto.userId, + total: dto.total, + }); + + return order; + } +} + +// Error handling patterns +// MessagePattern errors propagate to caller +@MessagePattern({ cmd: 'get_user' }) +async getUser(userId: string): Promise { + const user = await this.repo.findOne({ where: { id: userId } }); + if (!user) { + throw new RpcException('User not found'); // Received by caller + } + return user; +} + +// EventPattern errors should be handled locally +@EventPattern('order.created') +async handleOrderCreated(data: OrderCreatedEvent): Promise { + try { + await this.processOrder(data); + } catch (error) { + // Log and potentially retry - don't throw + this.logger.error('Failed to process order event', error); + await this.deadLetterQueue.add(data); + } +} +``` + +Reference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics) + +--- + +### 9.3 Use Message Queues for Background Jobs + +**Impact: MEDIUM-HIGH** — Queues enable reliable background processing + +Use `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests. + +**Incorrect (long-running tasks in HTTP handlers):** + +```typescript +// Long-running tasks in HTTP handlers +@Controller('reports') +export class ReportsController { + @Post() + async generate(@Body() dto: GenerateReportDto): Promise { + // This blocks the request for potentially minutes + const data = await this.fetchLargeDataset(dto); + const report = await this.processData(data); // Slow! + await this.sendEmail(dto.email, report); // Can fail! + return report; // Client times out + } +} + +// Fire-and-forget without retry +@Injectable() +export class EmailService { + async sendWelcome(email: string): Promise { + // If this fails, email is never sent + await this.mailer.send({ to: email, template: 'welcome' }); + // No retry, no tracking, no visibility + } +} + +// Use setInterval for scheduled tasks +setInterval(async () => { + await cleanupOldRecords(); +}, 60000); // No error handling, memory leaks +``` + +**Correct (use BullMQ for background processing):** + +```typescript +// Configure BullMQ +import { BullModule } from '@nestjs/bullmq'; + +@Module({ + imports: [ + BullModule.forRoot({ + connection: { + host: 'localhost', + port: 6379, + }, + defaultJobOptions: { + removeOnComplete: 1000, + removeOnFail: 5000, + attempts: 3, + backoff: { + type: 'exponential', + delay: 1000, + }, + }, + }), + BullModule.registerQueue( + { name: 'email' }, + { name: 'reports' }, + { name: 'notifications' }, + ), + ], +}) +export class QueueModule {} + +// Producer: Add jobs to queue +@Injectable() +export class ReportsService { + constructor( + @InjectQueue('reports') private reportsQueue: Queue, + ) {} + + async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> { + // Return immediately, process in background + const job = await this.reportsQueue.add('generate', dto, { + priority: dto.urgent ? 1 : 10, + delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0, + }); + + return { jobId: job.id }; + } + + async getJobStatus(jobId: string): Promise { + const job = await this.reportsQueue.getJob(jobId); + return { + status: await job.getState(), + progress: job.progress, + result: job.returnvalue, + }; + } +} + +// Consumer: Process jobs +@Processor('reports') +export class ReportsProcessor { + private readonly logger = new Logger(ReportsProcessor.name); + + @Process('generate') + async generateReport(job: Job): Promise { + this.logger.log(`Processing report job ${job.id}`); + + // Update progress + await job.updateProgress(10); + + const data = await this.fetchData(job.data); + await job.updateProgress(50); + + const report = await this.processData(data); + await job.updateProgress(90); + + await this.saveReport(report); + await job.updateProgress(100); + + return report; + } + + @OnQueueActive() + onActive(job: Job) { + this.logger.log(`Processing job ${job.id}`); + } + + @OnQueueCompleted() + onCompleted(job: Job, result: any) { + this.logger.log(`Job ${job.id} completed`); + } + + @OnQueueFailed() + onFailed(job: Job, error: Error) { + this.logger.error(`Job ${job.id} failed: ${error.message}`); + } +} + +// Email queue with retry +@Processor('email') +export class EmailProcessor { + @Process('send') + async sendEmail(job: Job): Promise { + const { to, template, data } = job.data; + + try { + await this.mailer.send({ + to, + template, + context: data, + }); + } catch (error) { + // BullMQ will retry based on job options + throw error; + } + } +} + +// Usage +@Injectable() +export class NotificationService { + constructor(@InjectQueue('email') private emailQueue: Queue) {} + + async sendWelcome(user: User): Promise { + await this.emailQueue.add( + 'send', + { + to: user.email, + template: 'welcome', + data: { name: user.name }, + }, + { + attempts: 5, + backoff: { type: 'exponential', delay: 5000 }, + }, + ); + } +} + +// Scheduled jobs +@Injectable() +export class ScheduledJobsService implements OnModuleInit { + constructor(@InjectQueue('maintenance') private queue: Queue) {} + + async onModuleInit(): Promise { + // Clean up old reports daily at midnight + await this.queue.add( + 'cleanup', + {}, + { + repeat: { cron: '0 0 * * *' }, + jobId: 'daily-cleanup', // Prevent duplicates + }, + ); + + // Send digest every hour + await this.queue.add( + 'digest', + {}, + { + repeat: { every: 60 * 60 * 1000 }, + jobId: 'hourly-digest', + }, + ); + } +} + +@Processor('maintenance') +export class MaintenanceProcessor { + @Process('cleanup') + async cleanup(): Promise { + await this.cleanupOldReports(); + await this.cleanupExpiredSessions(); + } + + @Process('digest') + async sendDigest(): Promise { + const users = await this.getUsersForDigest(); + for (const user of users) { + await this.emailQueue.add('send', { to: user.email, template: 'digest' }); + } + } +} + +// Queue monitoring with Bull Board +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; + +@Module({ + imports: [ + BullBoardModule.forRoot({ + route: '/admin/queues', + adapter: ExpressAdapter, + }), + BullBoardModule.forFeature({ + name: 'email', + adapter: BullMQAdapter, + }), + BullBoardModule.forFeature({ + name: 'reports', + adapter: BullMQAdapter, + }), + ], +}) +export class AdminModule {} +``` + +Reference: [NestJS Queues](https://docs.nestjs.com/techniques/queues) + +--- + +## 10. DevOps & Deployment + +**Section Impact: LOW-MEDIUM** + +### 10.1 Implement Graceful Shutdown + +**Impact: MEDIUM-HIGH** — Proper shutdown handling ensures zero-downtime deployments + +Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments. + +**Incorrect (ignoring shutdown signals):** + +```typescript +// Ignore shutdown signals +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); + // App crashes immediately on SIGTERM + // In-flight requests fail + // Database connections are abruptly closed +} + +// Long-running tasks without cancellation +@Injectable() +export class ProcessingService { + async processLargeFile(file: File): Promise { + // No way to interrupt this during shutdown + for (let i = 0; i < file.chunks.length; i++) { + await this.processChunk(file.chunks[i]); + // May run for minutes, blocking shutdown + } + } +} +``` + +**Correct (enable shutdown hooks and handle cleanup):** + +```typescript +// Enable shutdown hooks in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // Enable shutdown hooks + app.enableShutdownHooks(); + + // Optional: Add timeout for forced shutdown + const server = await app.listen(3000); + server.setTimeout(30000); // 30 second timeout + + // Handle graceful shutdown + const signals = ['SIGTERM', 'SIGINT']; + signals.forEach((signal) => { + process.on(signal, async () => { + console.log(`Received ${signal}, starting graceful shutdown...`); + + // Stop accepting new connections + server.close(async () => { + console.log('HTTP server closed'); + await app.close(); + process.exit(0); + }); + + // Force exit after timeout + setTimeout(() => { + console.error('Forced shutdown after timeout'); + process.exit(1); + }, 30000); + }); + }); +} + +// Lifecycle hooks for cleanup +@Injectable() +export class DatabaseService implements OnApplicationShutdown { + private readonly connections: Connection[] = []; + + async onApplicationShutdown(signal?: string): Promise { + console.log(`Database service shutting down on ${signal}`); + + // Close all connections gracefully + await Promise.all( + this.connections.map((conn) => conn.close()), + ); + + console.log('All database connections closed'); + } +} + +// Queue processor with graceful shutdown +@Injectable() +export class QueueService implements OnApplicationShutdown, OnModuleDestroy { + private isShuttingDown = false; + + onModuleDestroy(): void { + this.isShuttingDown = true; + } + + async onApplicationShutdown(): Promise { + // Wait for current jobs to complete + await this.queue.close(); + } + + async processJob(job: Job): Promise { + if (this.isShuttingDown) { + throw new Error('Service is shutting down'); + } + await this.doWork(job); + } +} + +// WebSocket gateway cleanup +@WebSocketGateway() +export class EventsGateway implements OnApplicationShutdown { + @WebSocketServer() + server: Server; + + async onApplicationShutdown(): Promise { + // Notify all connected clients + this.server.emit('shutdown', { message: 'Server is shutting down' }); + + // Close all connections + this.server.disconnectSockets(); + } +} + +// Health check integration +@Injectable() +export class ShutdownService { + private isShuttingDown = false; + + startShutdown(): void { + this.isShuttingDown = true; + } + + isShutdown(): boolean { + return this.isShuttingDown; + } +} + +@Controller('health') +export class HealthController { + constructor(private shutdownService: ShutdownService) {} + + @Get('ready') + @HealthCheck() + readiness(): Promise { + // Return 503 during shutdown - k8s stops sending traffic + if (this.shutdownService.isShutdown()) { + throw new ServiceUnavailableException('Shutting down'); + } + + return this.health.check([ + () => this.db.pingCheck('database'), + ]); + } +} + +// Integrate with shutdown +@Injectable() +export class AppShutdownService implements OnApplicationShutdown { + constructor(private shutdownService: ShutdownService) {} + + async onApplicationShutdown(): Promise { + // Mark as unhealthy first + this.shutdownService.startShutdown(); + + // Wait for k8s to update endpoints + await this.sleep(5000); + + // Then proceed with cleanup + } +} + +// Request tracking for in-flight requests +@Injectable() +export class RequestTracker implements NestMiddleware, OnApplicationShutdown { + private activeRequests = 0; + private isShuttingDown = false; + private shutdownPromise: Promise | null = null; + private resolveShutdown: (() => void) | null = null; + + use(req: Request, res: Response, next: NextFunction): void { + if (this.isShuttingDown) { + res.status(503).send('Service Unavailable'); + return; + } + + this.activeRequests++; + + res.on('finish', () => { + this.activeRequests--; + if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) { + this.resolveShutdown(); + } + }); + + next(); + } + + async onApplicationShutdown(): Promise { + this.isShuttingDown = true; + + if (this.activeRequests > 0) { + console.log(`Waiting for ${this.activeRequests} requests to complete`); + this.shutdownPromise = new Promise((resolve) => { + this.resolveShutdown = resolve; + }); + + // Wait with timeout + await Promise.race([ + this.shutdownPromise, + new Promise((resolve) => setTimeout(resolve, 30000)), + ]); + } + + console.log('All requests completed'); + } +} +``` + +Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events) + +--- + +### 10.2 Use ConfigModule for Environment Configuration + +**Impact: LOW-MEDIUM** — Proper configuration prevents deployment failures + +Use `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety. + +**Incorrect (accessing process.env directly):** + +```typescript +// Access process.env directly +@Injectable() +export class DatabaseService { + constructor() { + // No validation, can fail at runtime + this.connection = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), // NaN if missing + password: process.env.DB_PASSWORD, // undefined if missing + }); + } +} + +// Scattered env access +@Injectable() +export class EmailService { + sendEmail() { + // Different services access env differently + const apiKey = process.env.SENDGRID_API_KEY || 'default'; + // Typos go unnoticed: process.env.SENDGRID_API_KY + } +} +``` + +**Correct (use @nestjs/config with validation):** + +```typescript +// Setup validated configuration +import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; + +// config/database.config.ts +export const databaseConfig = registerAs('database', () => ({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT, 10), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, +})); + +// config/app.config.ts +export const appConfig = registerAs('app', () => ({ + port: parseInt(process.env.PORT, 10) || 3000, + environment: process.env.NODE_ENV || 'development', + apiPrefix: process.env.API_PREFIX || 'api', +})); + +// config/validation.schema.ts +export const validationSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'production', 'test') + .default('development'), + PORT: Joi.number().default(3000), + DB_HOST: Joi.string().required(), + DB_PORT: Joi.number().default(5432), + DB_USERNAME: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + DB_NAME: Joi.string().required(), + JWT_SECRET: Joi.string().min(32).required(), + REDIS_URL: Joi.string().uri().required(), +}); + +// app.module.ts +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, // Available everywhere without importing + load: [databaseConfig, appConfig], + validationSchema, + validationOptions: { + abortEarly: true, // Stop on first error + allowUnknown: true, // Allow other env vars + }, + }), + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('database.host'), + port: config.get('database.port'), + username: config.get('database.username'), + password: config.get('database.password'), + database: config.get('database.database'), + autoLoadEntities: true, + }), + }), + ], +}) +export class AppModule {} + +// Type-safe configuration access +export interface AppConfig { + port: number; + environment: 'development' | 'production' | 'test'; + apiPrefix: string; +} + +export interface DatabaseConfig { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// Type-safe access +@Injectable() +export class AppService { + constructor(private config: ConfigService) {} + + getPort(): number { + // Type-safe with generic + return this.config.get('app.port'); + } + + getDatabaseConfig(): DatabaseConfig { + return this.config.get('database'); + } +} + +// Inject namespaced config directly +@Injectable() +export class DatabaseService { + constructor( + @Inject(databaseConfig.KEY) + private dbConfig: ConfigType, + ) { + // Full type inference! + const host = this.dbConfig.host; // string + const port = this.dbConfig.port; // number + } +} + +// Environment files support +ConfigModule.forRoot({ + envFilePath: [ + `.env.${process.env.NODE_ENV}.local`, + `.env.${process.env.NODE_ENV}`, + '.env.local', + '.env', + ], +}); + +// .env.development +// DB_HOST=localhost +// DB_PORT=5432 + +// .env.production +// DB_HOST=prod-db.example.com +// DB_PORT=5432 +``` + +Reference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration) + +--- + +### 10.3 Use Structured Logging + +**Impact: MEDIUM-HIGH** — Structured logging enables effective debugging and monitoring + +Use NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels. + +**Incorrect (using console.log in production):** + +```typescript +// Use console.log in production +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + console.log('Creating user:', dto); + // Not structured, no levels, lost in production logs + + try { + const user = await this.repo.save(dto); + console.log('User created:', user.id); + return user; + } catch (error) { + console.log('Error:', error); // Using log for errors + throw error; + } + } +} + +// Log sensitive data +console.log('Login attempt:', { email, password }); // SECURITY RISK! + +// Inconsistent log format +logger.log('User ' + userId + ' created at ' + new Date()); +// Hard to parse, no structure +``` + +**Correct (use structured logging with context):** + +```typescript +// Configure logger in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + logger: + process.env.NODE_ENV === 'production' + ? ['error', 'warn', 'log'] + : ['error', 'warn', 'log', 'debug', 'verbose'], + }); +} + +// Use NestJS Logger with context +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + async createUser(dto: CreateUserDto): Promise { + this.logger.log('Creating user', { email: dto.email }); + + try { + const user = await this.repo.save(dto); + this.logger.log('User created', { userId: user.id }); + return user; + } catch (error) { + this.logger.error('Failed to create user', error.stack, { + email: dto.email, + }); + throw error; + } + } +} + +// Custom logger for JSON output +@Injectable() +export class JsonLogger implements LoggerService { + log(message: string, context?: object): void { + console.log( + JSON.stringify({ + level: 'info', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } + + error(message: string, trace?: string, context?: object): void { + console.error( + JSON.stringify({ + level: 'error', + timestamp: new Date().toISOString(), + message, + trace, + ...context, + }), + ); + } + + warn(message: string, context?: object): void { + console.warn( + JSON.stringify({ + level: 'warn', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } + + debug(message: string, context?: object): void { + console.debug( + JSON.stringify({ + level: 'debug', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } +} + +// Request context logging with ClsModule +import { ClsModule, ClsService } from 'nestjs-cls'; + +@Module({ + imports: [ + ClsModule.forRoot({ + global: true, + middleware: { + mount: true, + generateId: true, + }, + }), + ], +}) +export class AppModule {} + +// Middleware to set request context +@Injectable() +export class RequestContextMiddleware implements NestMiddleware { + constructor(private cls: ClsService) {} + + use(req: Request, res: Response, next: NextFunction): void { + const requestId = req.headers['x-request-id'] || randomUUID(); + this.cls.set('requestId', requestId); + this.cls.set('userId', req.user?.id); + + res.setHeader('x-request-id', requestId); + next(); + } +} + +// Logger that includes request context +@Injectable() +export class ContextLogger { + constructor(private cls: ClsService) {} + + log(message: string, data?: object): void { + console.log( + JSON.stringify({ + level: 'info', + timestamp: new Date().toISOString(), + requestId: this.cls.get('requestId'), + userId: this.cls.get('userId'), + message, + ...data, + }), + ); + } + + error(message: string, error: Error, data?: object): void { + console.error( + JSON.stringify({ + level: 'error', + timestamp: new Date().toISOString(), + requestId: this.cls.get('requestId'), + userId: this.cls.get('userId'), + message, + error: error.message, + stack: error.stack, + ...data, + }), + ); + } +} + +// Pino integration for high-performance logging +import { LoggerModule } from 'nestjs-pino'; + +@Module({ + imports: [ + LoggerModule.forRoot({ + pinoHttp: { + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + transport: + process.env.NODE_ENV !== 'production' + ? { target: 'pino-pretty' } + : undefined, + redact: ['req.headers.authorization', 'req.body.password'], + serializers: { + req: (req) => ({ + method: req.method, + url: req.url, + query: req.query, + }), + res: (res) => ({ + statusCode: res.statusCode, + }), + }, + }, + }), + ], +}) +export class AppModule {} + +// Usage with Pino +@Injectable() +export class UsersService { + constructor(private logger: PinoLogger) { + this.logger.setContext(UsersService.name); + } + + async findOne(id: string): Promise { + this.logger.info({ userId: id }, 'Finding user'); + // Pino uses first arg for data, second for message + } +} +``` + +Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger) + +--- + +## References + +- https://docs.nestjs.com +- https://github.com/nestjs/nest +- https://typeorm.io +- https://github.com/typestack/class-validator +- https://github.com/goldbergyoni/nodebestpractices + +--- + +*Generated by build-agents.ts on 2026-01-16* diff --git a/.agents/skills/nestjs-best-practices/SKILL.md b/.agents/skills/nestjs-best-practices/SKILL.md new file mode 100644 index 000000000..de75eac99 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/SKILL.md @@ -0,0 +1,130 @@ +--- +name: nestjs-best-practices +description: NestJS best practices and architecture patterns for building production-ready applications. This skill should be used when writing, reviewing, or refactoring NestJS code to ensure proper patterns for modules, dependency injection, security, and performance. +license: MIT +metadata: + author: Kadajett + version: "1.1.0" +--- + +# NestJS Best Practices + +Comprehensive best practices guide for NestJS applications. Contains 40 rules across 10 categories, prioritized by impact to guide automated refactoring and code generation. + +## When to Apply + +Reference these guidelines when: + +- Writing new NestJS modules, controllers, or services +- Implementing authentication and authorization +- Reviewing code for architecture and security issues +- Refactoring existing NestJS codebases +- Optimizing performance or database queries +- Building microservices architectures + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Architecture | CRITICAL | `arch-` | +| 2 | Dependency Injection | CRITICAL | `di-` | +| 3 | Error Handling | HIGH | `error-` | +| 4 | Security | HIGH | `security-` | +| 5 | Performance | HIGH | `perf-` | +| 6 | Testing | MEDIUM-HIGH | `test-` | +| 7 | Database & ORM | MEDIUM-HIGH | `db-` | +| 8 | API Design | MEDIUM | `api-` | +| 9 | Microservices | MEDIUM | `micro-` | +| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` | + +## Quick Reference + +### 1. Architecture (CRITICAL) + +- `arch-avoid-circular-deps` - Avoid circular module dependencies +- `arch-feature-modules` - Organize by feature, not technical layer +- `arch-module-sharing` - Proper module exports/imports, avoid duplicate providers +- `arch-single-responsibility` - Focused services over "god services" +- `arch-use-repository-pattern` - Abstract database logic for testability +- `arch-use-events` - Event-driven architecture for decoupling + +### 2. Dependency Injection (CRITICAL) + +- `di-avoid-service-locator` - Avoid service locator anti-pattern +- `di-interface-segregation` - Interface Segregation Principle (ISP) +- `di-liskov-substitution` - Liskov Substitution Principle (LSP) +- `di-prefer-constructor-injection` - Constructor over property injection +- `di-scope-awareness` - Understand singleton/request/transient scopes +- `di-use-interfaces-tokens` - Use injection tokens for interfaces + +### 3. Error Handling (HIGH) + +- `error-use-exception-filters` - Centralized exception handling +- `error-throw-http-exceptions` - Use NestJS HTTP exceptions +- `error-handle-async-errors` - Handle async errors properly + +### 4. Security (HIGH) + +- `security-auth-jwt` - Secure JWT authentication +- `security-validate-all-input` - Validate with class-validator +- `security-use-guards` - Authentication and authorization guards +- `security-sanitize-output` - Prevent XSS attacks +- `security-rate-limiting` - Implement rate limiting + +### 5. Performance (HIGH) + +- `perf-async-hooks` - Proper async lifecycle hooks +- `perf-use-caching` - Implement caching strategies +- `perf-optimize-database` - Optimize database queries +- `perf-lazy-loading` - Lazy load modules for faster startup + +### 6. Testing (MEDIUM-HIGH) + +- `test-use-testing-module` - Use NestJS testing utilities +- `test-e2e-supertest` - E2E testing with Supertest +- `test-mock-external-services` - Mock external dependencies + +### 7. Database & ORM (MEDIUM-HIGH) + +- `db-use-transactions` - Transaction management +- `db-avoid-n-plus-one` - Avoid N+1 query problems +- `db-use-migrations` - Use migrations for schema changes + +### 8. API Design (MEDIUM) + +- `api-use-dto-serialization` - DTO and response serialization +- `api-use-interceptors` - Cross-cutting concerns +- `api-versioning` - API versioning strategies +- `api-use-pipes` - Input transformation with pipes + +### 9. Microservices (MEDIUM) + +- `micro-use-patterns` - Message and event patterns +- `micro-use-health-checks` - Health checks for orchestration +- `micro-use-queues` - Background job processing + +### 10. DevOps & Deployment (LOW-MEDIUM) + +- `devops-use-config-module` - Environment configuration +- `devops-use-logging` - Structured logging +- `devops-graceful-shutdown` - Zero-downtime deployments + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +rules/arch-avoid-circular-deps.md +rules/security-validate-all-input.md +rules/_sections.md +``` + +Each rule file contains: +- Brief explanation of why it matters +- Incorrect code example with explanation +- Correct code example with explanation +- Additional context and references + +## Full Compiled Document + +For the complete guide with all rules expanded: `AGENTS.md` diff --git a/.agents/skills/nestjs-best-practices/rules/_sections.md b/.agents/skills/nestjs-best-practices/rules/_sections.md new file mode 100644 index 000000000..91bc8029a --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/_sections.md @@ -0,0 +1,56 @@ +# Sections + +This file defines all sections, their ordering, impact levels, and descriptions. +The section ID (in parentheses) is the filename prefix used to group rules. + +--- + +## 1. Architecture (arch) + +**Impact:** CRITICAL +**Description:** Proper module organization and dependency management are the foundation of maintainable NestJS applications. Circular dependencies and god services are the #1 architecture killer. + +## 2. Dependency Injection (di) + +**Impact:** CRITICAL +**Description:** NestJS's IoC container is powerful but can be misused. Understanding scopes, injection tokens, and proper patterns is essential for testable code. + +## 3. Error Handling (error) + +**Impact:** HIGH +**Description:** Consistent error handling improves debugging, user experience, and API reliability. Centralized exception filters ensure uniform error responses. + +## 4. Security (security) + +**Impact:** HIGH +**Description:** Security vulnerabilities can be catastrophic. Input validation, authentication, authorization, and data protection are non-negotiable. + +## 5. Performance (perf) + +**Impact:** HIGH +**Description:** Optimizing request handling, caching, and database queries directly impacts application responsiveness and scalability. + +## 6. Testing (test) + +**Impact:** MEDIUM-HIGH +**Description:** Well-tested applications are more reliable. NestJS testing utilities enable comprehensive unit and e2e coverage. + +## 7. Database & ORM (db) + +**Impact:** MEDIUM-HIGH +**Description:** Proper database access patterns, transactions, and query optimization are crucial for data-intensive applications. + +## 8. API Design (api) + +**Impact:** MEDIUM +**Description:** RESTful conventions, versioning, DTOs, and consistent response formats improve API usability and maintainability. + +## 9. Microservices (micro) + +**Impact:** MEDIUM +**Description:** Building distributed systems requires understanding message patterns, health checks, and inter-service communication. + +## 10. DevOps & Deployment (devops) + +**Impact:** LOW-MEDIUM +**Description:** Configuration management, structured logging, and graceful shutdown ensure production readiness and zero-downtime deployments. diff --git a/.agents/skills/nestjs-best-practices/rules/_template.md b/.agents/skills/nestjs-best-practices/rules/_template.md new file mode 100644 index 000000000..1e9e70703 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/_template.md @@ -0,0 +1,28 @@ +--- +title: Rule Title Here +impact: MEDIUM +impactDescription: Optional description of impact (e.g., "20-50% improvement") +tags: tag1, tag2 +--- + +## Rule Title Here + +**Impact: MEDIUM (optional impact description)** + +Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications. + +**Incorrect (description of what's wrong):** + +```typescript +// Bad code example here +const bad = example() +``` + +**Correct (description of what's right):** + +```typescript +// Good code example here +const good = example() +``` + +Reference: [Link to documentation or resource](https://example.com) diff --git a/.agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md b/.agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md new file mode 100644 index 000000000..be95c07f7 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md @@ -0,0 +1,182 @@ +--- +title: Use DTOs and Serialization for API Responses +impact: MEDIUM +impactDescription: Response DTOs prevent accidental data exposure and ensure consistency +tags: api, dto, serialization, class-transformer +--- + +## Use DTOs and Serialization for API Responses + +Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract. + +**Incorrect (returning entities directly or manual spreading):** + +```typescript +// Return entities directly +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + // Returns: { id, email, passwordHash, ssn, internalNotes, ... } + // Exposes sensitive data! + } +} + +// Manual object spreading (error-prone) +@Get(':id') +async findOne(@Param('id') id: string) { + const user = await this.usersService.findById(id); + return { + id: user.id, + email: user.email, + name: user.name, + // Easy to forget to exclude sensitive fields + // Hard to maintain across endpoints + }; +} +``` + +**Correct (use class-transformer with @Exclude and response DTOs):** + +```typescript +// Enable class-transformer globally +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); + await app.listen(3000); +} + +// Entity with serialization control +@Entity() +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + email: string; + + @Column() + name: string; + + @Column() + @Exclude() // Never include in responses + passwordHash: string; + + @Column({ nullable: true }) + @Exclude() + ssn: string; + + @Column({ default: false }) + @Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests + isAdmin: boolean; + + @CreateDateColumn() + createdAt: Date; + + @Column() + @Exclude() + internalNotes: string; +} + +// Now returning entity is safe +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + // Returns: { id, email, name, createdAt } + // Sensitive fields excluded automatically + } +} + +// For different response shapes, use explicit DTOs +export class UserResponseDto { + @Expose() + id: string; + + @Expose() + email: string; + + @Expose() + name: string; + + @Expose() + @Transform(({ obj }) => obj.posts?.length || 0) + postCount: number; + + constructor(partial: Partial) { + Object.assign(this, partial); + } +} + +export class UserDetailResponseDto extends UserResponseDto { + @Expose() + createdAt: Date; + + @Expose() + @Type(() => PostResponseDto) + posts: PostResponseDto[]; +} + +// Controller with explicit DTOs +@Controller('users') +export class UsersController { + @Get() + @SerializeOptions({ type: UserResponseDto }) + async findAll(): Promise { + const users = await this.usersService.findAll(); + return users.map(u => plainToInstance(UserResponseDto, u)); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findByIdWithPosts(id); + return plainToInstance(UserDetailResponseDto, user, { + excludeExtraneousValues: true, + }); + } +} + +// Groups for conditional serialization +export class UserDto { + @Expose() + id: string; + + @Expose() + name: string; + + @Expose({ groups: ['admin'] }) + email: string; + + @Expose({ groups: ['admin'] }) + createdAt: Date; + + @Expose({ groups: ['admin', 'owner'] }) + settings: UserSettings; +} + +@Controller('users') +export class UsersController { + @Get() + @SerializeOptions({ groups: ['public'] }) + async findAllPublic(): Promise { + // Returns: { id, name } + } + + @Get('admin') + @UseGuards(AdminGuard) + @SerializeOptions({ groups: ['admin'] }) + async findAllAdmin(): Promise { + // Returns: { id, name, email, createdAt } + } + + @Get('me') + @SerializeOptions({ groups: ['owner'] }) + async getProfile(@CurrentUser() user: User): Promise { + // Returns: { id, name, settings } + } +} +``` + +Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization) diff --git a/.agents/skills/nestjs-best-practices/rules/api-use-interceptors.md b/.agents/skills/nestjs-best-practices/rules/api-use-interceptors.md new file mode 100644 index 000000000..522ab3d52 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/api-use-interceptors.md @@ -0,0 +1,202 @@ +--- +title: Use Interceptors for Cross-Cutting Concerns +impact: MEDIUM-HIGH +impactDescription: Interceptors provide clean separation for cross-cutting logic +tags: api, interceptors, logging, caching +--- + +## Use Interceptors for Cross-Cutting Concerns + +Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams. + +**Incorrect (logging and transformation in every method):** + +```typescript +// Logging in every controller method +@Controller('users') +export class UsersController { + @Get() + async findAll(): Promise { + const start = Date.now(); + this.logger.log('findAll called'); + + const users = await this.usersService.findAll(); + + this.logger.log(`findAll completed in ${Date.now() - start}ms`); + return users; + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const start = Date.now(); + this.logger.log(`findOne called with id: ${id}`); + + const user = await this.usersService.findOne(id); + + this.logger.log(`findOne completed in ${Date.now() - start}ms`); + return user; + } + // Repeated in every method! +} + +// Manual response wrapping +@Get() +async findAll(): Promise<{ data: User[]; meta: Meta }> { + const users = await this.usersService.findAll(); + return { + data: users, + meta: { timestamp: new Date(), count: users.length }, + }; +} +``` + +**Correct (use interceptors for cross-cutting concerns):** + +```typescript +// Logging interceptor +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private readonly logger = new Logger('HTTP'); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest(); + const { method, url, body } = request; + const now = Date.now(); + + return next.handle().pipe( + tap({ + next: (data) => { + const response = context.switchToHttp().getResponse(); + this.logger.log( + `${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`, + ); + }, + error: (error) => { + this.logger.error( + `${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`, + error.stack, + ); + }, + }), + ); + } +} + +// Response transformation interceptor +@Injectable() +export class TransformInterceptor implements NestInterceptor> { + intercept(context: ExecutionContext, next: CallHandler): Observable> { + return next.handle().pipe( + map((data) => ({ + data, + meta: { + timestamp: new Date().toISOString(), + path: context.switchToHttp().getRequest().url, + }, + })), + ); + } +} + +// Timeout interceptor +@Injectable() +export class TimeoutInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + timeout(5000), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException('Request timed out'); + } + throw err; + }), + ); + } +} + +// Apply globally or per-controller +@Module({ + providers: [ + { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, + { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, + ], +}) +export class AppModule {} + +// Or per-controller +@Controller('users') +@UseInterceptors(LoggingInterceptor) +export class UsersController { + @Get() + async findAll(): Promise { + // Clean business logic only + return this.usersService.findAll(); + } +} + +// Custom cache interceptor with TTL +@Injectable() +export class HttpCacheInterceptor implements NestInterceptor { + constructor( + private cacheManager: Cache, + private reflector: Reflector, + ) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request = context.switchToHttp().getRequest(); + + // Only cache GET requests + if (request.method !== 'GET') { + return next.handle(); + } + + const cacheKey = this.generateKey(request); + const ttl = this.reflector.get('cacheTTL', context.getHandler()) || 300; + + const cached = await this.cacheManager.get(cacheKey); + if (cached) { + return of(cached); + } + + return next.handle().pipe( + tap((response) => { + this.cacheManager.set(cacheKey, response, ttl); + }), + ); + } + + private generateKey(request: Request): string { + return `cache:${request.url}:${JSON.stringify(request.query)}`; + } +} + +// Usage with custom TTL +@Get() +@SetMetadata('cacheTTL', 600) +@UseInterceptors(HttpCacheInterceptor) +async findAll(): Promise { + return this.usersService.findAll(); +} + +// Error mapping interceptor +@Injectable() +export class ErrorMappingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + catchError((error) => { + if (error instanceof EntityNotFoundError) { + throw new NotFoundException(error.message); + } + if (error instanceof QueryFailedError) { + if (error.message.includes('duplicate')) { + throw new ConflictException('Resource already exists'); + } + } + throw error; + }), + ); + } +} +``` + +Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors) diff --git a/.agents/skills/nestjs-best-practices/rules/api-use-pipes.md b/.agents/skills/nestjs-best-practices/rules/api-use-pipes.md new file mode 100644 index 000000000..72b9a6e9d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/api-use-pipes.md @@ -0,0 +1,205 @@ +--- +title: Use Pipes for Input Transformation +impact: MEDIUM +impactDescription: Pipes ensure clean, validated data reaches your handlers +tags: api, pipes, validation, transformation +--- + +## Use Pipes for Input Transformation + +Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers. + +**Incorrect (manual type parsing in handlers):** + +```typescript +// Manual type parsing in handlers +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + // Manual validation in every handler + const uuid = id.trim(); + if (!isUUID(uuid)) { + throw new BadRequestException('Invalid UUID'); + } + return this.usersService.findOne(uuid); + } + + @Get() + async findAll( + @Query('page') page: string, + @Query('limit') limit: string, + ): Promise { + // Manual parsing and defaults + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + return this.usersService.findAll(pageNum, limitNum); + } +} + +// Type coercion without validation +@Get() +async search(@Query('price') price: string): Promise { + const priceNum = +price; // NaN if invalid, no error + return this.productsService.findByPrice(priceNum); +} +``` + +**Correct (use built-in and custom pipes):** + +```typescript +// Use built-in pipes for common transformations +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { + // id is guaranteed to be a valid UUID + return this.usersService.findOne(id); + } + + @Get() + async findAll( + @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, + @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, + ): Promise { + // Automatic defaults and type conversion + return this.usersService.findAll(page, limit); + } + + @Get('by-status/:status') + async findByStatus( + @Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus, + ): Promise { + return this.usersService.findByStatus(status); + } +} + +// Custom pipe for business logic +@Injectable() +export class ParseDatePipe implements PipeTransform { + transform(value: string): Date { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new BadRequestException('Invalid date format'); + } + return date; + } +} + +@Get('reports') +async getReports( + @Query('from', ParseDatePipe) from: Date, + @Query('to', ParseDatePipe) to: Date, +): Promise { + return this.reportsService.findBetween(from, to); +} + +// Custom transformation pipes +@Injectable() +export class NormalizeEmailPipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + return value.trim().toLowerCase(); + } +} + +// Parse comma-separated values +@Injectable() +export class ParseArrayPipe implements PipeTransform { + transform(value: string): string[] { + if (!value) return []; + return value.split(',').map((v) => v.trim()).filter(Boolean); + } +} + +@Get('products') +async findProducts( + @Query('ids', ParseArrayPipe) ids: string[], + @Query('email', NormalizeEmailPipe) email: string, +): Promise { + // ids is already an array, email is normalized + return this.productsService.findByIds(ids); +} + +// Sanitize HTML input +@Injectable() +export class SanitizeHtmlPipe implements PipeTransform { + transform(value: string): string { + if (!value) return value; + return sanitizeHtml(value, { allowedTags: [] }); + } +} + +// Global validation pipe with transformation +app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strip non-DTO properties + transform: true, // Auto-transform to DTO types + transformOptions: { + enableImplicitConversion: true, // Convert query strings to numbers + }, + forbidNonWhitelisted: true, // Throw on extra properties + }), +); + +// DTO with transformation decorators +export class FindProductsDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 10; + + @IsOptional() + @Transform(({ value }) => value?.toLowerCase()) + @IsString() + search?: string; + + @IsOptional() + @Transform(({ value }) => value?.split(',')) + @IsArray() + @IsString({ each: true }) + categories?: string[]; +} + +@Get() +async findAll(@Query() dto: FindProductsDto): Promise { + // dto is already transformed and validated + return this.productsService.findAll(dto); +} + +// Pipe error customization +@Injectable() +export class CustomParseIntPipe extends ParseIntPipe { + constructor() { + super({ + exceptionFactory: (error) => + new BadRequestException(`${error} must be a valid integer`), + }); + } +} + +// Or use options on built-in pipes +@Get(':id') +async findOne( + @Param( + 'id', + new ParseIntPipe({ + errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, + exceptionFactory: () => new NotAcceptableException('ID must be numeric'), + }), + ) + id: number, +): Promise { + return this.itemsService.findOne(id); +} +``` + +Reference: [NestJS Pipes](https://docs.nestjs.com/pipes) diff --git a/.agents/skills/nestjs-best-practices/rules/api-versioning.md b/.agents/skills/nestjs-best-practices/rules/api-versioning.md new file mode 100644 index 000000000..5e4546ca0 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/api-versioning.md @@ -0,0 +1,191 @@ +--- +title: Use API Versioning for Breaking Changes +impact: MEDIUM +impactDescription: Versioning allows you to evolve APIs without breaking existing clients +tags: api, versioning, breaking-changes, compatibility +--- + +## Use API Versioning for Breaking Changes + +Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints. + +**Incorrect (breaking changes without versioning):** + +```typescript +// Breaking changes without versioning +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + // Original response: { id, name, email } + // Later changed to: { id, firstName, lastName, emailAddress } + // Old clients break! + return this.usersService.findOne(id); + } +} + +// Manual versioning in routes +@Controller('v1/users') +export class UsersV1Controller {} + +@Controller('v2/users') +export class UsersV2Controller {} +// Inconsistent, error-prone, hard to maintain +``` + +**Correct (use NestJS built-in versioning):** + +```typescript +// Enable versioning in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // URI versioning: /v1/users, /v2/users + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: '1', + }); + + // Or header versioning: X-API-Version: 1 + app.enableVersioning({ + type: VersioningType.HEADER, + header: 'X-API-Version', + defaultVersion: '1', + }); + + // Or media type: Accept: application/json;v=1 + app.enableVersioning({ + type: VersioningType.MEDIA_TYPE, + key: 'v=', + defaultVersion: '1', + }); + + await app.listen(3000); +} + +// Version-specific controllers +@Controller('users') +@Version('1') +export class UsersV1Controller { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findOne(id); + // V1 response format + return { + id: user.id, + name: user.name, + email: user.email, + }; + } +} + +@Controller('users') +@Version('2') +export class UsersV2Controller { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findOne(id); + // V2 response format with breaking changes + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + emailAddress: user.email, + createdAt: user.createdAt, + }; + } +} + +// Per-route versioning - different versions for different routes +@Controller('users') +export class UsersController { + @Get() + @Version('1') + findAllV1(): Promise { + return this.usersService.findAllV1(); + } + + @Get() + @Version('2') + findAllV2(): Promise { + return this.usersService.findAllV2(); + } + + @Get(':id') + @Version(['1', '2']) // Same handler for multiple versions + findOne(@Param('id') id: string): Promise { + return this.usersService.findOne(id); + } + + @Post() + @Version(VERSION_NEUTRAL) // Available in all versions + create(@Body() dto: CreateUserDto): Promise { + return this.usersService.create(dto); + } +} + +// Shared service with version-specific logic +@Injectable() +export class UsersService { + async findOne(id: string, version: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + + if (version === '1') { + return this.toV1Response(user); + } + return this.toV2Response(user); + } + + private toV1Response(user: User): UserV1Response { + return { + id: user.id, + name: `${user.firstName} ${user.lastName}`, + email: user.email, + }; + } + + private toV2Response(user: User): UserV2Response { + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + emailAddress: user.email, + createdAt: user.createdAt, + }; + } +} + +// Controller extracts version +@Controller('users') +export class UsersController { + @Get(':id') + async findOne( + @Param('id') id: string, + @Headers('X-API-Version') version: string = '1', + ): Promise { + return this.usersService.findOne(id, version); + } +} + +// Deprecation strategy - mark old versions as deprecated +@Controller('users') +@Version('1') +@UseInterceptors(DeprecationInterceptor) +export class UsersV1Controller { + // All V1 routes will include deprecation warning +} + +@Injectable() +export class DeprecationInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const response = context.switchToHttp().getResponse(); + response.setHeader('Deprecation', 'true'); + response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT'); + response.setHeader('Link', '; rel="successor-version"'); + + return next.handle(); + } +} +``` + +Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md b/.agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md new file mode 100644 index 000000000..aea5c8be5 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md @@ -0,0 +1,80 @@ +--- +title: Avoid Circular Dependencies +impact: CRITICAL +impactDescription: "#1 cause of runtime crashes" +tags: architecture, modules, dependencies +--- + +## Avoid Circular Dependencies + +Circular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications. + +**Incorrect (circular module imports):** + +```typescript +// users.module.ts +@Module({ + imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} + +// orders.module.ts +@Module({ + imports: [UsersModule], // Circular dependency! + providers: [OrdersService], + exports: [OrdersService], +}) +export class OrdersModule {} +``` + +**Correct (extract shared logic or use events):** + +```typescript +// Option 1: Extract shared logic to a third module +// shared.module.ts +@Module({ + providers: [SharedService], + exports: [SharedService], +}) +export class SharedModule {} + +// users.module.ts +@Module({ + imports: [SharedModule], + providers: [UsersService], +}) +export class UsersModule {} + +// orders.module.ts +@Module({ + imports: [SharedModule], + providers: [OrdersService], +}) +export class OrdersModule {} + +// Option 2: Use events for decoupled communication +// users.service.ts +@Injectable() +export class UsersService { + constructor(private eventEmitter: EventEmitter2) {} + + async createUser(data: CreateUserDto) { + const user = await this.userRepo.save(data); + this.eventEmitter.emit('user.created', user); + return user; + } +} + +// orders.service.ts +@Injectable() +export class OrdersService { + @OnEvent('user.created') + handleUserCreated(user: User) { + // React to user creation without direct dependency + } +} +``` + +Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-feature-modules.md b/.agents/skills/nestjs-best-practices/rules/arch-feature-modules.md new file mode 100644 index 000000000..4f3ef5679 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-feature-modules.md @@ -0,0 +1,82 @@ +--- +title: Organize by Feature Modules +impact: CRITICAL +impactDescription: "3-5x faster onboarding and development" +tags: architecture, modules, organization +--- + +## Organize by Feature Modules + +Organize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development. + +**Incorrect (technical layer organization):** + +```typescript +// Technical layer organization (anti-pattern) +src/ +├── controllers/ +│ ├── users.controller.ts +│ ├── orders.controller.ts +│ └── products.controller.ts +├── services/ +│ ├── users.service.ts +│ ├── orders.service.ts +│ └── products.service.ts +├── entities/ +│ ├── user.entity.ts +│ ├── order.entity.ts +│ └── product.entity.ts +└── app.module.ts // Imports everything directly +``` + +**Correct (feature module organization):** + +```typescript +// Feature module organization +src/ +├── users/ +│ ├── dto/ +│ │ ├── create-user.dto.ts +│ │ └── update-user.dto.ts +│ ├── entities/ +│ │ └── user.entity.ts +│ ├── users.controller.ts +│ ├── users.service.ts +│ ├── users.repository.ts +│ └── users.module.ts +├── orders/ +│ ├── dto/ +│ ├── entities/ +│ ├── orders.controller.ts +│ ├── orders.service.ts +│ └── orders.module.ts +├── shared/ +│ ├── guards/ +│ ├── interceptors/ +│ ├── filters/ +│ └── shared.module.ts +└── app.module.ts + +// users.module.ts +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersService], // Only export what others need +}) +export class UsersModule {} + +// app.module.ts +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forRoot(), + UsersModule, + OrdersModule, + SharedModule, + ], +}) +export class AppModule {} +``` + +Reference: [NestJS Modules](https://docs.nestjs.com/modules) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-module-sharing.md b/.agents/skills/nestjs-best-practices/rules/arch-module-sharing.md new file mode 100644 index 000000000..06ab76786 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-module-sharing.md @@ -0,0 +1,141 @@ +--- +title: Use Proper Module Sharing Patterns +impact: CRITICAL +impactDescription: Prevents duplicate instances, memory leaks, and state inconsistency +tags: architecture, modules, sharing, exports +--- + +## Use Proper Module Sharing Patterns + +NestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed. + +**Incorrect (service provided in multiple modules):** + +```typescript +// StorageService provided directly in multiple modules - WRONG +// storage.service.ts +@Injectable() +export class StorageService { + private cache = new Map(); // Each instance has separate state! + + store(key: string, value: any) { + this.cache.set(key, value); + } +} + +// app.module.ts +@Module({ + providers: [StorageService], // Instance #1 + controllers: [AppController], +}) +export class AppModule {} + +// videos.module.ts +@Module({ + providers: [StorageService], // Instance #2 - different from AppModule! + controllers: [VideosController], +}) +export class VideosModule {} + +// Problems: +// 1. Two separate StorageService instances exist +// 2. cache.set() in VideosModule doesn't affect AppModule's cache +// 3. Memory wasted on duplicate instances +// 4. Debugging nightmares when state doesn't sync +``` + +**Correct (dedicated module with exports):** + +```typescript +// storage/storage.module.ts +@Module({ + providers: [StorageService], + exports: [StorageService], // Make available to importers +}) +export class StorageModule {} + +// videos/videos.module.ts +@Module({ + imports: [StorageModule], // Import the module, not the service + controllers: [VideosController], + providers: [VideosService], +}) +export class VideosModule {} + +// channels/channels.module.ts +@Module({ + imports: [StorageModule], // Same instance shared + controllers: [ChannelsController], + providers: [ChannelsService], +}) +export class ChannelsModule {} + +// app.module.ts +@Module({ + imports: [ + StorageModule, // Only if AppModule itself needs StorageService + VideosModule, + ChannelsModule, + ], +}) +export class AppModule {} + +// Now all modules share the SAME StorageService instance +``` + +**When to use @Global() (sparingly):** + +```typescript +// ONLY for truly cross-cutting concerns +@Global() +@Module({ + providers: [ConfigService, LoggerService], + exports: [ConfigService, LoggerService], +}) +export class CoreModule {} + +// Import once in AppModule +@Module({ + imports: [CoreModule], // Registered globally, available everywhere +}) +export class AppModule {} + +// Other modules don't need to import CoreModule +@Module({ + controllers: [UsersController], + providers: [UsersService], // Can inject ConfigService without importing +}) +export class UsersModule {} + +// WARNING: Don't make everything global! +// - Hides dependencies (can't see what a module needs from imports) +// - Makes testing harder +// - Reserve for: config, logging, database connections +``` + +**Module re-exporting pattern:** + +```typescript +// common.module.ts - shared utilities +@Module({ + providers: [DateService, ValidationService], + exports: [DateService, ValidationService], +}) +export class CommonModule {} + +// core.module.ts - re-exports common for convenience +@Module({ + imports: [CommonModule, DatabaseModule], + exports: [CommonModule, DatabaseModule], // Re-export for consumers +}) +export class CoreModule {} + +// feature.module.ts - imports CoreModule, gets both +@Module({ + imports: [CoreModule], // Gets CommonModule + DatabaseModule + controllers: [FeatureController], +}) +export class FeatureModule {} +``` + +Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md b/.agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md new file mode 100644 index 000000000..6ffc46eb2 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md @@ -0,0 +1,106 @@ +--- +title: Single Responsibility for Services +impact: CRITICAL +impactDescription: "40%+ improvement in testability" +tags: architecture, services, single-responsibility +--- + +## Single Responsibility for Services + +Each service should have a single, well-defined responsibility. Avoid "god services" that handle multiple unrelated concerns. If a service name includes "And" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+. + +**Incorrect (god service anti-pattern):** + +```typescript +// God service anti-pattern +@Injectable() +export class UserAndOrderService { + constructor( + private userRepo: UserRepository, + private orderRepo: OrderRepository, + private mailer: MailService, + private payment: PaymentService, + ) {} + + async createUser(dto: CreateUserDto) { + const user = await this.userRepo.save(dto); + await this.mailer.sendWelcome(user); + return user; + } + + async createOrder(userId: string, dto: CreateOrderDto) { + const order = await this.orderRepo.save({ userId, ...dto }); + await this.payment.charge(order); + await this.mailer.sendOrderConfirmation(order); + return order; + } + + async calculateOrderStats(userId: string) { + // Stats logic mixed in + } + + async validatePayment(orderId: string) { + // Payment logic mixed in + } +} +``` + +**Correct (focused services with single responsibility):** + +```typescript +// Focused services with single responsibility +@Injectable() +export class UsersService { + constructor(private userRepo: UserRepository) {} + + async create(dto: CreateUserDto): Promise { + return this.userRepo.save(dto); + } + + async findById(id: string): Promise { + return this.userRepo.findOneOrFail({ where: { id } }); + } +} + +@Injectable() +export class OrdersService { + constructor(private orderRepo: OrderRepository) {} + + async create(userId: string, dto: CreateOrderDto): Promise { + return this.orderRepo.save({ userId, ...dto }); + } + + async findByUser(userId: string): Promise { + return this.orderRepo.find({ where: { userId } }); + } +} + +@Injectable() +export class OrderStatsService { + constructor(private orderRepo: OrderRepository) {} + + async calculateForUser(userId: string): Promise { + // Focused stats calculation + } +} + +// Orchestration in controller or dedicated orchestrator +@Controller('orders') +export class OrdersController { + constructor( + private orders: OrdersService, + private payment: PaymentService, + private notifications: NotificationService, + ) {} + + @Post() + async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) { + const order = await this.orders.create(user.id, dto); + await this.payment.charge(order); + await this.notifications.sendOrderConfirmation(order); + return order; + } +} +``` + +Reference: [NestJS Providers](https://docs.nestjs.com/providers) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-use-events.md b/.agents/skills/nestjs-best-practices/rules/arch-use-events.md new file mode 100644 index 000000000..f8cda270f --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-use-events.md @@ -0,0 +1,108 @@ +--- +title: Use Event-Driven Architecture for Decoupling +impact: MEDIUM-HIGH +impactDescription: Enables async processing and modularity +tags: architecture, events, decoupling +--- + +## Use Event-Driven Architecture for Decoupling + +Use `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing. + +**Incorrect (direct service coupling):** + +```typescript +// Direct service coupling +@Injectable() +export class OrdersService { + constructor( + private inventoryService: InventoryService, + private emailService: EmailService, + private analyticsService: AnalyticsService, + private notificationService: NotificationService, + private loyaltyService: LoyaltyService, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Tight coupling - OrdersService knows about all consumers + await this.inventoryService.reserve(order.items); + await this.emailService.sendConfirmation(order); + await this.analyticsService.track('order_created', order); + await this.notificationService.push(order.userId, 'Order placed'); + await this.loyaltyService.addPoints(order.userId, order.total); + + // Adding new behavior requires modifying this service + return order; + } +} +``` + +**Correct (event-driven decoupling):** + +```typescript +// Use EventEmitter for decoupling +import { EventEmitter2 } from '@nestjs/event-emitter'; + +// Define event +export class OrderCreatedEvent { + constructor( + public readonly orderId: string, + public readonly userId: string, + public readonly items: OrderItem[], + public readonly total: number, + ) {} +} + +// Service emits events +@Injectable() +export class OrdersService { + constructor( + private eventEmitter: EventEmitter2, + private repo: Repository, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Emit event - no knowledge of consumers + this.eventEmitter.emit( + 'order.created', + new OrderCreatedEvent(order.id, order.userId, order.items, order.total), + ); + + return order; + } +} + +// Listeners in separate modules +@Injectable() +export class InventoryListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.inventoryService.reserve(event.items); + } +} + +@Injectable() +export class EmailListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.emailService.sendConfirmation(event.orderId); + } +} + +@Injectable() +export class AnalyticsListener { + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + await this.analyticsService.track('order_created', { + orderId: event.orderId, + total: event.total, + }); + } +} +``` + +Reference: [NestJS Events](https://docs.nestjs.com/techniques/events) diff --git a/.agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md b/.agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md new file mode 100644 index 000000000..75df38101 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md @@ -0,0 +1,97 @@ +--- +title: Use Repository Pattern for Data Access +impact: HIGH +impactDescription: Decouples business logic from database +tags: architecture, repository, data-access +--- + +## Use Repository Pattern for Data Access + +Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code. + +**Incorrect (complex queries in services):** + +```typescript +// Complex queries in services +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) private repo: Repository, + ) {} + + async findActiveWithOrders(minOrders: number): Promise { + // Complex query logic mixed with business logic + return this.repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.orders', 'order') + .where('user.isActive = :active', { active: true }) + .andWhere('user.deletedAt IS NULL') + .groupBy('user.id') + .having('COUNT(order.id) >= :min', { min: minOrders }) + .orderBy('user.createdAt', 'DESC') + .getMany(); + } + + // Service becomes bloated with query logic +} +``` + +**Correct (custom repository with encapsulated queries):** + +```typescript +// Custom repository with encapsulated queries +@Injectable() +export class UsersRepository { + constructor( + @InjectRepository(User) private repo: Repository, + ) {} + + async findById(id: string): Promise { + return this.repo.findOne({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.repo.findOne({ where: { email } }); + } + + async findActiveWithMinOrders(minOrders: number): Promise { + return this.repo + .createQueryBuilder('user') + .leftJoinAndSelect('user.orders', 'order') + .where('user.isActive = :active', { active: true }) + .andWhere('user.deletedAt IS NULL') + .groupBy('user.id') + .having('COUNT(order.id) >= :min', { min: minOrders }) + .orderBy('user.createdAt', 'DESC') + .getMany(); + } + + async save(user: User): Promise { + return this.repo.save(user); + } +} + +// Clean service with business logic only +@Injectable() +export class UsersService { + constructor(private usersRepo: UsersRepository) {} + + async getActiveUsersWithOrders(): Promise { + return this.usersRepo.findActiveWithMinOrders(1); + } + + async create(dto: CreateUserDto): Promise { + const existing = await this.usersRepo.findByEmail(dto.email); + if (existing) { + throw new ConflictException('Email already registered'); + } + + const user = new User(); + user.email = dto.email; + user.name = dto.name; + return this.usersRepo.save(user); + } +} +``` + +Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html) diff --git a/.agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md b/.agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md new file mode 100644 index 000000000..a93ec4b59 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md @@ -0,0 +1,139 @@ +--- +title: Avoid N+1 Query Problems +impact: HIGH +impactDescription: N+1 queries are one of the most common performance killers +tags: database, n-plus-one, queries, performance +--- + +## Avoid N+1 Query Problems + +N+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently. + +**Incorrect (lazy loading in loops causes N+1):** + +```typescript +// Lazy loading in loops causes N+1 +@Injectable() +export class OrdersService { + async getOrdersWithItems(userId: string): Promise { + const orders = await this.orderRepo.find({ where: { userId } }); + // 1 query for orders + + for (const order of orders) { + // N additional queries - one per order! + order.items = await this.itemRepo.find({ where: { orderId: order.id } }); + } + + return orders; + } +} + +// Accessing lazy relations without loading +@Controller('users') +export class UsersController { + @Get() + async findAll(): Promise { + const users = await this.userRepo.find(); + // If User.posts is lazy-loaded, serializing triggers N queries + return users; // Each user.posts access = 1 query + } +} +``` + +**Correct (use relations for eager loading):** + +```typescript +// Use relations option for eager loading +@Injectable() +export class OrdersService { + async getOrdersWithItems(userId: string): Promise { + // Single query with JOIN + return this.orderRepo.find({ + where: { userId }, + relations: ['items', 'items.product'], + }); + } +} + +// Use QueryBuilder for complex joins +@Injectable() +export class UsersService { + async getUsersWithPostCounts(): Promise { + return this.userRepo + .createQueryBuilder('user') + .leftJoin('user.posts', 'post') + .select('user.id', 'id') + .addSelect('user.name', 'name') + .addSelect('COUNT(post.id)', 'postCount') + .groupBy('user.id') + .getRawMany(); + } + + async getActiveUsersWithPosts(): Promise { + return this.userRepo + .createQueryBuilder('user') + .leftJoinAndSelect('user.posts', 'post') + .leftJoinAndSelect('post.comments', 'comment') + .where('user.isActive = :active', { active: true }) + .andWhere('post.status = :status', { status: 'published' }) + .getMany(); + } +} + +// Use find options for specific fields +async getOrderSummaries(userId: string): Promise { + return this.orderRepo.find({ + where: { userId }, + relations: ['items'], + select: { + id: true, + total: true, + status: true, + items: { + id: true, + quantity: true, + price: true, + }, + }, + }); +} + +// Use DataLoader for GraphQL to batch and cache queries +import DataLoader from 'dataloader'; + +@Injectable({ scope: Scope.REQUEST }) +export class PostsLoader { + constructor(private postsService: PostsService) {} + + readonly batchPosts = new DataLoader(async (userIds) => { + // Single query for all users' posts + const posts = await this.postsService.findByUserIds([...userIds]); + + // Group by userId + const postsMap = new Map(); + for (const post of posts) { + const userPosts = postsMap.get(post.userId) || []; + userPosts.push(post); + postsMap.set(post.userId, userPosts); + } + + // Return in same order as input + return userIds.map((id) => postsMap.get(id) || []); + }); +} + +// In resolver +@ResolveField() +async posts(@Parent() user: User): Promise { + // DataLoader batches multiple calls into single query + return this.postsLoader.batchPosts.load(user.id); +} + +// Enable query logging in development to detect N+1 +TypeOrmModule.forRoot({ + logging: ['query', 'error'], + logger: 'advanced-console', +}); +``` + +Reference: [TypeORM Relations](https://typeorm.io/relations) diff --git a/.agents/skills/nestjs-best-practices/rules/db-use-migrations.md b/.agents/skills/nestjs-best-practices/rules/db-use-migrations.md new file mode 100644 index 000000000..4c3b72408 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/db-use-migrations.md @@ -0,0 +1,129 @@ +--- +title: Use Database Migrations +impact: HIGH +impactDescription: Enables safe, repeatable database schema changes +tags: database, migrations, typeorm, schema +--- + +## Use Database Migrations + +Never use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments. + +**Incorrect (using synchronize or manual SQL):** + +```typescript +// Use synchronize in production +TypeOrmModule.forRoot({ + type: 'postgres', + synchronize: true, // DANGEROUS in production! + // Can drop columns, tables, or data +}); + +// Manual SQL in production +@Injectable() +export class DatabaseService { + async addColumn(): Promise { + await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT'); + // No version control, no rollback, inconsistent across envs + } +} + +// Modify entities without migration +@Entity() +export class User { + @Column() + email: string; + + @Column() // Added without migration + newField: string; // Will crash in production if synchronize is false +} +``` + +**Correct (use migrations for all schema changes):** + +```typescript +// Configure TypeORM for migrations +// data-source.ts +export const dataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: ['dist/**/*.entity.js'], + migrations: ['dist/migrations/*.js'], + synchronize: false, // Always false in production + migrationsRun: true, // Run migrations on startup +}); + +// app.module.ts +TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('DB_HOST'), + synchronize: config.get('NODE_ENV') === 'development', // Only in dev + migrations: ['dist/migrations/*.js'], + migrationsRun: true, + }), +}); + +// migrations/1705312800000-AddUserAge.ts +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserAge1705312800000 implements MigrationInterface { + name = 'AddUserAge1705312800000'; + + public async up(queryRunner: QueryRunner): Promise { + // Add column with default to handle existing rows + await queryRunner.query(` + ALTER TABLE "users" ADD "age" integer DEFAULT 0 + `); + + // Add index for frequently queried columns + await queryRunner.query(` + CREATE INDEX "IDX_users_age" ON "users" ("age") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Always implement down for rollback + await queryRunner.query(`DROP INDEX "IDX_users_age"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "age"`); + } +} + +// Safe column rename (two-step) +export class RenameNameToFullName1705312900000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Step 1: Add new column + await queryRunner.query(` + ALTER TABLE "users" ADD "full_name" varchar(255) + `); + + // Step 2: Copy data + await queryRunner.query(` + UPDATE "users" SET "full_name" = "name" + `); + + // Step 3: Add NOT NULL constraint + await queryRunner.query(` + ALTER TABLE "users" ALTER COLUMN "full_name" SET NOT NULL + `); + + // Step 4: Drop old column (after verifying app works) + await queryRunner.query(` + ALTER TABLE "users" DROP COLUMN "name" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "name" varchar(255)`); + await queryRunner.query(`UPDATE "users" SET "name" = "full_name"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "full_name"`); + } +} +``` + +Reference: [TypeORM Migrations](https://typeorm.io/migrations) diff --git a/.agents/skills/nestjs-best-practices/rules/db-use-transactions.md b/.agents/skills/nestjs-best-practices/rules/db-use-transactions.md new file mode 100644 index 000000000..543bf974f --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/db-use-transactions.md @@ -0,0 +1,140 @@ +--- +title: Use Transactions for Multi-Step Operations +impact: HIGH +impactDescription: Ensures data consistency in multi-step operations +tags: database, transactions, typeorm, consistency +--- + +## Use Transactions for Multi-Step Operations + +When multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios. + +**Incorrect (multiple saves without transaction):** + +```typescript +// Multiple saves without transaction +@Injectable() +export class OrdersService { + async createOrder(userId: string, items: OrderItem[]): Promise { + // If any step fails, data is inconsistent + const order = await this.orderRepo.save({ userId, status: 'pending' }); + + for (const item of items) { + await this.orderItemRepo.save({ orderId: order.id, ...item }); + await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity); + } + + await this.paymentService.charge(order.id); + // If payment fails, order and inventory are already modified! + + return order; + } +} +``` + +**Correct (use DataSource.transaction for automatic rollback):** + +```typescript +// Use DataSource.transaction() for automatic rollback +@Injectable() +export class OrdersService { + constructor(private dataSource: DataSource) {} + + async createOrder(userId: string, items: OrderItem[]): Promise { + return this.dataSource.transaction(async (manager) => { + // All operations use the same transactional manager + const order = await manager.save(Order, { userId, status: 'pending' }); + + for (const item of items) { + await manager.save(OrderItem, { orderId: order.id, ...item }); + await manager.decrement( + Inventory, + { productId: item.productId }, + 'stock', + item.quantity, + ); + } + + // If this throws, everything rolls back + await this.paymentService.chargeWithManager(manager, order.id); + + return order; + }); + } +} + +// QueryRunner for manual transaction control +@Injectable() +export class TransferService { + constructor(private dataSource: DataSource) {} + + async transfer(fromId: string, toId: string, amount: number): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Debit source account + await queryRunner.manager.decrement( + Account, + { id: fromId }, + 'balance', + amount, + ); + + // Verify sufficient funds + const source = await queryRunner.manager.findOne(Account, { + where: { id: fromId }, + }); + if (source.balance < 0) { + throw new BadRequestException('Insufficient funds'); + } + + // Credit destination account + await queryRunner.manager.increment( + Account, + { id: toId }, + 'balance', + amount, + ); + + // Log the transaction + await queryRunner.manager.save(TransactionLog, { + fromId, + toId, + amount, + timestamp: new Date(), + }); + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } +} + +// Repository method with transaction support +@Injectable() +export class UsersRepository { + constructor( + @InjectRepository(User) private repo: Repository, + private dataSource: DataSource, + ) {} + + async createWithProfile( + userData: CreateUserDto, + profileData: CreateProfileDto, + ): Promise { + return this.dataSource.transaction(async (manager) => { + const user = await manager.save(User, userData); + await manager.save(Profile, { ...profileData, userId: user.id }); + return user; + }); + } +} +``` + +Reference: [TypeORM Transactions](https://typeorm.io/transactions) diff --git a/.agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md b/.agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md new file mode 100644 index 000000000..b3b18f820 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md @@ -0,0 +1,222 @@ +--- +title: Implement Graceful Shutdown +impact: MEDIUM-HIGH +impactDescription: Proper shutdown handling ensures zero-downtime deployments +tags: devops, graceful-shutdown, lifecycle, kubernetes +--- + +## Implement Graceful Shutdown + +Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments. + +**Incorrect (ignoring shutdown signals):** + +```typescript +// Ignore shutdown signals +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); + // App crashes immediately on SIGTERM + // In-flight requests fail + // Database connections are abruptly closed +} + +// Long-running tasks without cancellation +@Injectable() +export class ProcessingService { + async processLargeFile(file: File): Promise { + // No way to interrupt this during shutdown + for (let i = 0; i < file.chunks.length; i++) { + await this.processChunk(file.chunks[i]); + // May run for minutes, blocking shutdown + } + } +} +``` + +**Correct (enable shutdown hooks and handle cleanup):** + +```typescript +// Enable shutdown hooks in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // Enable shutdown hooks + app.enableShutdownHooks(); + + // Optional: Add timeout for forced shutdown + const server = await app.listen(3000); + server.setTimeout(30000); // 30 second timeout + + // Handle graceful shutdown + const signals = ['SIGTERM', 'SIGINT']; + signals.forEach((signal) => { + process.on(signal, async () => { + console.log(`Received ${signal}, starting graceful shutdown...`); + + // Stop accepting new connections + server.close(async () => { + console.log('HTTP server closed'); + await app.close(); + process.exit(0); + }); + + // Force exit after timeout + setTimeout(() => { + console.error('Forced shutdown after timeout'); + process.exit(1); + }, 30000); + }); + }); +} + +// Lifecycle hooks for cleanup +@Injectable() +export class DatabaseService implements OnApplicationShutdown { + private readonly connections: Connection[] = []; + + async onApplicationShutdown(signal?: string): Promise { + console.log(`Database service shutting down on ${signal}`); + + // Close all connections gracefully + await Promise.all( + this.connections.map((conn) => conn.close()), + ); + + console.log('All database connections closed'); + } +} + +// Queue processor with graceful shutdown +@Injectable() +export class QueueService implements OnApplicationShutdown, OnModuleDestroy { + private isShuttingDown = false; + + onModuleDestroy(): void { + this.isShuttingDown = true; + } + + async onApplicationShutdown(): Promise { + // Wait for current jobs to complete + await this.queue.close(); + } + + async processJob(job: Job): Promise { + if (this.isShuttingDown) { + throw new Error('Service is shutting down'); + } + await this.doWork(job); + } +} + +// WebSocket gateway cleanup +@WebSocketGateway() +export class EventsGateway implements OnApplicationShutdown { + @WebSocketServer() + server: Server; + + async onApplicationShutdown(): Promise { + // Notify all connected clients + this.server.emit('shutdown', { message: 'Server is shutting down' }); + + // Close all connections + this.server.disconnectSockets(); + } +} + +// Health check integration +@Injectable() +export class ShutdownService { + private isShuttingDown = false; + + startShutdown(): void { + this.isShuttingDown = true; + } + + isShutdown(): boolean { + return this.isShuttingDown; + } +} + +@Controller('health') +export class HealthController { + constructor(private shutdownService: ShutdownService) {} + + @Get('ready') + @HealthCheck() + readiness(): Promise { + // Return 503 during shutdown - k8s stops sending traffic + if (this.shutdownService.isShutdown()) { + throw new ServiceUnavailableException('Shutting down'); + } + + return this.health.check([ + () => this.db.pingCheck('database'), + ]); + } +} + +// Integrate with shutdown +@Injectable() +export class AppShutdownService implements OnApplicationShutdown { + constructor(private shutdownService: ShutdownService) {} + + async onApplicationShutdown(): Promise { + // Mark as unhealthy first + this.shutdownService.startShutdown(); + + // Wait for k8s to update endpoints + await this.sleep(5000); + + // Then proceed with cleanup + } +} + +// Request tracking for in-flight requests +@Injectable() +export class RequestTracker implements NestMiddleware, OnApplicationShutdown { + private activeRequests = 0; + private isShuttingDown = false; + private shutdownPromise: Promise | null = null; + private resolveShutdown: (() => void) | null = null; + + use(req: Request, res: Response, next: NextFunction): void { + if (this.isShuttingDown) { + res.status(503).send('Service Unavailable'); + return; + } + + this.activeRequests++; + + res.on('finish', () => { + this.activeRequests--; + if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) { + this.resolveShutdown(); + } + }); + + next(); + } + + async onApplicationShutdown(): Promise { + this.isShuttingDown = true; + + if (this.activeRequests > 0) { + console.log(`Waiting for ${this.activeRequests} requests to complete`); + this.shutdownPromise = new Promise((resolve) => { + this.resolveShutdown = resolve; + }); + + // Wait with timeout + await Promise.race([ + this.shutdownPromise, + new Promise((resolve) => setTimeout(resolve, 30000)), + ]); + } + + console.log('All requests completed'); + } +} +``` + +Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events) diff --git a/.agents/skills/nestjs-best-practices/rules/devops-use-config-module.md b/.agents/skills/nestjs-best-practices/rules/devops-use-config-module.md new file mode 100644 index 000000000..a9483dedf --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/devops-use-config-module.md @@ -0,0 +1,167 @@ +--- +title: Use ConfigModule for Environment Configuration +impact: LOW-MEDIUM +impactDescription: Proper configuration prevents deployment failures +tags: devops, configuration, environment, validation +--- + +## Use ConfigModule for Environment Configuration + +Use `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety. + +**Incorrect (accessing process.env directly):** + +```typescript +// Access process.env directly +@Injectable() +export class DatabaseService { + constructor() { + // No validation, can fail at runtime + this.connection = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), // NaN if missing + password: process.env.DB_PASSWORD, // undefined if missing + }); + } +} + +// Scattered env access +@Injectable() +export class EmailService { + sendEmail() { + // Different services access env differently + const apiKey = process.env.SENDGRID_API_KEY || 'default'; + // Typos go unnoticed: process.env.SENDGRID_API_KY + } +} +``` + +**Correct (use @nestjs/config with validation):** + +```typescript +// Setup validated configuration +import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; +import * as Joi from 'joi'; + +// config/database.config.ts +export const databaseConfig = registerAs('database', () => ({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT, 10), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, +})); + +// config/app.config.ts +export const appConfig = registerAs('app', () => ({ + port: parseInt(process.env.PORT, 10) || 3000, + environment: process.env.NODE_ENV || 'development', + apiPrefix: process.env.API_PREFIX || 'api', +})); + +// config/validation.schema.ts +export const validationSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'production', 'test') + .default('development'), + PORT: Joi.number().default(3000), + DB_HOST: Joi.string().required(), + DB_PORT: Joi.number().default(5432), + DB_USERNAME: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + DB_NAME: Joi.string().required(), + JWT_SECRET: Joi.string().min(32).required(), + REDIS_URL: Joi.string().uri().required(), +}); + +// app.module.ts +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, // Available everywhere without importing + load: [databaseConfig, appConfig], + validationSchema, + validationOptions: { + abortEarly: true, // Stop on first error + allowUnknown: true, // Allow other env vars + }, + }), + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('database.host'), + port: config.get('database.port'), + username: config.get('database.username'), + password: config.get('database.password'), + database: config.get('database.database'), + autoLoadEntities: true, + }), + }), + ], +}) +export class AppModule {} + +// Type-safe configuration access +export interface AppConfig { + port: number; + environment: 'development' | 'production' | 'test'; + apiPrefix: string; +} + +export interface DatabaseConfig { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// Type-safe access +@Injectable() +export class AppService { + constructor(private config: ConfigService) {} + + getPort(): number { + // Type-safe with generic + return this.config.get('app.port'); + } + + getDatabaseConfig(): DatabaseConfig { + return this.config.get('database'); + } +} + +// Inject namespaced config directly +@Injectable() +export class DatabaseService { + constructor( + @Inject(databaseConfig.KEY) + private dbConfig: ConfigType, + ) { + // Full type inference! + const host = this.dbConfig.host; // string + const port = this.dbConfig.port; // number + } +} + +// Environment files support +ConfigModule.forRoot({ + envFilePath: [ + `.env.${process.env.NODE_ENV}.local`, + `.env.${process.env.NODE_ENV}`, + '.env.local', + '.env', + ], +}); + +// .env.development +// DB_HOST=localhost +// DB_PORT=5432 + +// .env.production +// DB_HOST=prod-db.example.com +// DB_PORT=5432 +``` + +Reference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration) diff --git a/.agents/skills/nestjs-best-practices/rules/devops-use-logging.md b/.agents/skills/nestjs-best-practices/rules/devops-use-logging.md new file mode 100644 index 000000000..5fb0162bd --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/devops-use-logging.md @@ -0,0 +1,232 @@ +--- +title: Use Structured Logging +impact: MEDIUM-HIGH +impactDescription: Structured logging enables effective debugging and monitoring +tags: devops, logging, structured-logs, pino +--- + +## Use Structured Logging + +Use NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels. + +**Incorrect (using console.log in production):** + +```typescript +// Use console.log in production +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + console.log('Creating user:', dto); + // Not structured, no levels, lost in production logs + + try { + const user = await this.repo.save(dto); + console.log('User created:', user.id); + return user; + } catch (error) { + console.log('Error:', error); // Using log for errors + throw error; + } + } +} + +// Log sensitive data +console.log('Login attempt:', { email, password }); // SECURITY RISK! + +// Inconsistent log format +logger.log('User ' + userId + ' created at ' + new Date()); +// Hard to parse, no structure +``` + +**Correct (use structured logging with context):** + +```typescript +// Configure logger in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + logger: + process.env.NODE_ENV === 'production' + ? ['error', 'warn', 'log'] + : ['error', 'warn', 'log', 'debug', 'verbose'], + }); +} + +// Use NestJS Logger with context +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + async createUser(dto: CreateUserDto): Promise { + this.logger.log('Creating user', { email: dto.email }); + + try { + const user = await this.repo.save(dto); + this.logger.log('User created', { userId: user.id }); + return user; + } catch (error) { + this.logger.error('Failed to create user', error.stack, { + email: dto.email, + }); + throw error; + } + } +} + +// Custom logger for JSON output +@Injectable() +export class JsonLogger implements LoggerService { + log(message: string, context?: object): void { + console.log( + JSON.stringify({ + level: 'info', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } + + error(message: string, trace?: string, context?: object): void { + console.error( + JSON.stringify({ + level: 'error', + timestamp: new Date().toISOString(), + message, + trace, + ...context, + }), + ); + } + + warn(message: string, context?: object): void { + console.warn( + JSON.stringify({ + level: 'warn', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } + + debug(message: string, context?: object): void { + console.debug( + JSON.stringify({ + level: 'debug', + timestamp: new Date().toISOString(), + message, + ...context, + }), + ); + } +} + +// Request context logging with ClsModule +import { ClsModule, ClsService } from 'nestjs-cls'; + +@Module({ + imports: [ + ClsModule.forRoot({ + global: true, + middleware: { + mount: true, + generateId: true, + }, + }), + ], +}) +export class AppModule {} + +// Middleware to set request context +@Injectable() +export class RequestContextMiddleware implements NestMiddleware { + constructor(private cls: ClsService) {} + + use(req: Request, res: Response, next: NextFunction): void { + const requestId = req.headers['x-request-id'] || randomUUID(); + this.cls.set('requestId', requestId); + this.cls.set('userId', req.user?.id); + + res.setHeader('x-request-id', requestId); + next(); + } +} + +// Logger that includes request context +@Injectable() +export class ContextLogger { + constructor(private cls: ClsService) {} + + log(message: string, data?: object): void { + console.log( + JSON.stringify({ + level: 'info', + timestamp: new Date().toISOString(), + requestId: this.cls.get('requestId'), + userId: this.cls.get('userId'), + message, + ...data, + }), + ); + } + + error(message: string, error: Error, data?: object): void { + console.error( + JSON.stringify({ + level: 'error', + timestamp: new Date().toISOString(), + requestId: this.cls.get('requestId'), + userId: this.cls.get('userId'), + message, + error: error.message, + stack: error.stack, + ...data, + }), + ); + } +} + +// Pino integration for high-performance logging +import { LoggerModule } from 'nestjs-pino'; + +@Module({ + imports: [ + LoggerModule.forRoot({ + pinoHttp: { + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + transport: + process.env.NODE_ENV !== 'production' + ? { target: 'pino-pretty' } + : undefined, + redact: ['req.headers.authorization', 'req.body.password'], + serializers: { + req: (req) => ({ + method: req.method, + url: req.url, + query: req.query, + }), + res: (res) => ({ + statusCode: res.statusCode, + }), + }, + }, + }), + ], +}) +export class AppModule {} + +// Usage with Pino +@Injectable() +export class UsersService { + constructor(private logger: PinoLogger) { + this.logger.setContext(UsersService.name); + } + + async findOne(id: string): Promise { + this.logger.info({ userId: id }, 'Finding user'); + // Pino uses first arg for data, second for message + } +} +``` + +Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger) diff --git a/.agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md b/.agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md new file mode 100644 index 000000000..d4c04b479 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md @@ -0,0 +1,104 @@ +--- +title: Avoid Service Locator Anti-Pattern +impact: HIGH +impactDescription: Hides dependencies and breaks testability +tags: dependency-injection, anti-patterns, testing +--- + +## Avoid Service Locator Anti-Pattern + +Avoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead. + +**Incorrect (service locator anti-pattern):** + +```typescript +// Use ModuleRef to get dependencies dynamically +@Injectable() +export class OrdersService { + constructor(private moduleRef: ModuleRef) {} + + async createOrder(dto: CreateOrderDto): Promise { + // Dependencies are hidden - not visible in constructor + const usersService = this.moduleRef.get(UsersService); + const inventoryService = this.moduleRef.get(InventoryService); + const paymentService = this.moduleRef.get(PaymentService); + + const user = await usersService.findOne(dto.userId); + // ... rest of logic + } +} + +// Global singleton container +class ServiceContainer { + private static instance: ServiceContainer; + private services = new Map(); + + static getInstance(): ServiceContainer { + if (!this.instance) { + this.instance = new ServiceContainer(); + } + return this.instance; + } + + get(key: string): T { + return this.services.get(key); + } +} +``` + +**Correct (constructor injection with explicit dependencies):** + +```typescript +// Use constructor injection - dependencies are explicit +@Injectable() +export class OrdersService { + constructor( + private usersService: UsersService, + private inventoryService: InventoryService, + private paymentService: PaymentService, + ) {} + + async createOrder(dto: CreateOrderDto): Promise { + const user = await this.usersService.findOne(dto.userId); + const inventory = await this.inventoryService.check(dto.items); + // Dependencies are clear and testable + } +} + +// Easy to test with mocks +describe('OrdersService', () => { + let service: OrdersService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + OrdersService, + { provide: UsersService, useValue: mockUsersService }, + { provide: InventoryService, useValue: mockInventoryService }, + { provide: PaymentService, useValue: mockPaymentService }, + ], + }).compile(); + + service = module.get(OrdersService); + }); +}); + +// VALID: Factory pattern for dynamic instantiation +@Injectable() +export class HandlerFactory { + constructor(private moduleRef: ModuleRef) {} + + getHandler(type: string): Handler { + switch (type) { + case 'email': + return this.moduleRef.get(EmailHandler); + case 'sms': + return this.moduleRef.get(SmsHandler); + default: + return this.moduleRef.get(DefaultHandler); + } + } +} +``` + +Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref) diff --git a/.agents/skills/nestjs-best-practices/rules/di-interface-segregation.md b/.agents/skills/nestjs-best-practices/rules/di-interface-segregation.md new file mode 100644 index 000000000..8c96cd814 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-interface-segregation.md @@ -0,0 +1,165 @@ +--- +title: Apply Interface Segregation Principle +impact: HIGH +impactDescription: Reduces coupling and improves testability by 30-50% +tags: dependency-injection, interfaces, solid, isp +--- + +## Apply Interface Segregation Principle + +Clients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating "fat" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones. + +**Incorrect (fat interface forcing unused dependencies):** + +```typescript +// Fat interface - forces all consumers to depend on everything +interface NotificationService { + sendEmail(to: string, subject: string, body: string): Promise; + sendSms(phone: string, message: string): Promise; + sendPush(userId: string, notification: PushPayload): Promise; + sendSlack(channel: string, message: string): Promise; + logNotification(type: string, payload: any): Promise; + getDeliveryStatus(id: string): Promise; + retryFailed(id: string): Promise; + scheduleNotification(dto: ScheduleDto): Promise; +} + +// Consumer only needs email, but must mock everything for tests +@Injectable() +export class OrdersService { + constructor( + private notifications: NotificationService, // Depends on 8 methods, uses 1 + ) {} + + async confirmOrder(order: Order): Promise { + await this.notifications.sendEmail( + order.customer.email, + 'Order Confirmed', + `Your order ${order.id} has been confirmed.`, + ); + } +} + +// Testing is painful - must mock unused methods +const mockNotificationService = { + sendEmail: jest.fn(), + sendSms: jest.fn(), // Never used, but required + sendPush: jest.fn(), // Never used, but required + sendSlack: jest.fn(), // Never used, but required + logNotification: jest.fn(), // Never used, but required + getDeliveryStatus: jest.fn(), // Never used, but required + retryFailed: jest.fn(), // Never used, but required + scheduleNotification: jest.fn(), // Never used, but required +}; +``` + +**Correct (segregated interfaces by capability):** + +```typescript +// Segregated interfaces - each focused on one capability +interface EmailSender { + sendEmail(to: string, subject: string, body: string): Promise; +} + +interface SmsSender { + sendSms(phone: string, message: string): Promise; +} + +interface PushSender { + sendPush(userId: string, notification: PushPayload): Promise; +} + +interface NotificationLogger { + logNotification(type: string, payload: any): Promise; +} + +interface NotificationScheduler { + scheduleNotification(dto: ScheduleDto): Promise; +} + +// Implementation can implement multiple interfaces +@Injectable() +export class NotificationService implements EmailSender, SmsSender, PushSender { + async sendEmail(to: string, subject: string, body: string): Promise { + // Email implementation + } + + async sendSms(phone: string, message: string): Promise { + // SMS implementation + } + + async sendPush(userId: string, notification: PushPayload): Promise { + // Push implementation + } +} + +// Or separate implementations +@Injectable() +export class SendGridEmailService implements EmailSender { + async sendEmail(to: string, subject: string, body: string): Promise { + // SendGrid-specific implementation + } +} + +// Consumer depends only on what it needs +@Injectable() +export class OrdersService { + constructor( + @Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency + ) {} + + async confirmOrder(order: Order): Promise { + await this.emailSender.sendEmail( + order.customer.email, + 'Order Confirmed', + `Your order ${order.id} has been confirmed.`, + ); + } +} + +// Testing is simple - only mock what's used +const mockEmailSender: EmailSender = { + sendEmail: jest.fn(), +}; + +// Module registration with tokens +export const EMAIL_SENDER = Symbol('EMAIL_SENDER'); +export const SMS_SENDER = Symbol('SMS_SENDER'); + +@Module({ + providers: [ + { provide: EMAIL_SENDER, useClass: SendGridEmailService }, + { provide: SMS_SENDER, useClass: TwilioSmsService }, + ], + exports: [EMAIL_SENDER, SMS_SENDER], +}) +export class NotificationModule {} +``` + +**Combining interfaces when needed:** + +```typescript +// Sometimes a consumer legitimately needs multiple capabilities +interface EmailAndSmsSender extends EmailSender, SmsSender {} + +// Or use intersection types +type MultiChannelSender = EmailSender & SmsSender & PushSender; + +// Consumer that genuinely needs multiple channels +@Injectable() +export class AlertService { + constructor( + @Inject(MULTI_CHANNEL_SENDER) + private sender: EmailSender & SmsSender, + ) {} + + async sendCriticalAlert(user: User, message: string): Promise { + await Promise.all([ + this.sender.sendEmail(user.email, 'Critical Alert', message), + this.sender.sendSms(user.phone, message), + ]); + } +} +``` + +Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle) diff --git a/.agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md b/.agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md new file mode 100644 index 000000000..d6701178d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md @@ -0,0 +1,221 @@ +--- +title: Honor Liskov Substitution Principle +impact: HIGH +impactDescription: Ensures implementations are truly interchangeable without breaking callers +tags: dependency-injection, inheritance, solid, lsp +--- + +## Honor Liskov Substitution Principle + +Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations. + +**Incorrect (implementation violates the contract):** + +```typescript +// Base interface with clear contract +interface PaymentGateway { + /** + * Charges the specified amount. + * @returns PaymentResult on success + * @throws PaymentFailedException on payment failure + */ + charge(amount: number, currency: string): Promise; +} + +// Production implementation - follows the contract +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + const response = await this.stripe.charges.create({ amount, currency }); + return { success: true, transactionId: response.id, amount }; + } +} + +// Mock that violates LSP - different behavior! +@Injectable() +export class MockPaymentService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + // VIOLATION 1: Throws for valid input (contract says return PaymentResult) + if (amount > 1000) { + throw new Error('Mock does not support large amounts'); + } + + // VIOLATION 2: Returns null instead of PaymentResult + if (currency !== 'USD') { + return null as any; // Real service would convert or reject properly + } + + // VIOLATION 3: Missing required field + return { success: true } as PaymentResult; // Missing transactionId! + } +} + +// Consumer trusts the contract +@Injectable() +export class OrdersService { + constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} + + async checkout(order: Order): Promise { + const result = await this.payment.charge(order.total, order.currency); + // These fail with MockPaymentService: + await this.saveTransaction(result.transactionId); // undefined! + await this.sendReceipt(result); // might be null! + } +} +``` + +**Correct (implementations honor the contract):** + +```typescript +// Well-defined interface with documented behavior +interface PaymentGateway { + /** + * Charges the specified amount. + * @param amount - Amount in smallest currency unit (cents) + * @param currency - ISO 4217 currency code + * @returns PaymentResult with transactionId, success status, and amount + * @throws PaymentFailedException if charge is declined + * @throws InvalidCurrencyException if currency is not supported + */ + charge(amount: number, currency: string): Promise; + + /** + * Refunds a previous charge. + * @throws TransactionNotFoundException if transactionId is invalid + */ + refund(transactionId: string, amount?: number): Promise; +} + +// Production implementation +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number, currency: string): Promise { + try { + const response = await this.stripe.charges.create({ amount, currency }); + return { + success: true, + transactionId: response.id, + amount: response.amount, + }; + } catch (error) { + if (error.type === 'card_error') { + throw new PaymentFailedException(error.message); + } + throw error; + } + } + + async refund(transactionId: string, amount?: number): Promise { + // Implementation... + } +} + +// Mock that honors LSP - same contract, same behavior shape +@Injectable() +export class MockPaymentService implements PaymentGateway { + private transactions = new Map(); + + async charge(amount: number, currency: string): Promise { + // Honor the contract: validate currency like real service would + if (!['USD', 'EUR', 'GBP'].includes(currency)) { + throw new InvalidCurrencyException(`Unsupported currency: ${currency}`); + } + + // Simulate decline for specific test scenarios + if (amount === 99999) { + throw new PaymentFailedException('Card declined (test scenario)'); + } + + // Return same shape as production + const result: PaymentResult = { + success: true, + transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`, + amount, + }; + + this.transactions.set(result.transactionId, result); + return result; + } + + async refund(transactionId: string, amount?: number): Promise { + // Honor the contract: throw if transaction not found + if (!this.transactions.has(transactionId)) { + throw new TransactionNotFoundException(transactionId); + } + + return { + success: true, + refundId: `refund_${transactionId}`, + amount: amount ?? this.transactions.get(transactionId)!.amount, + }; + } +} + +// Consumer can swap implementations safely +@Injectable() +export class OrdersService { + constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {} + + async checkout(order: Order): Promise { + try { + const result = await this.payment.charge(order.total, order.currency); + // Works with both StripeService and MockPaymentService + order.transactionId = result.transactionId; + order.status = 'paid'; + return order; + } catch (error) { + if (error instanceof PaymentFailedException) { + order.status = 'payment_failed'; + return order; + } + throw error; + } + } +} +``` + +**Testing LSP compliance:** + +```typescript +// Shared test suite that any implementation must pass +function testPaymentGatewayContract( + createGateway: () => PaymentGateway, +) { + describe('PaymentGateway contract', () => { + let gateway: PaymentGateway; + + beforeEach(() => { + gateway = createGateway(); + }); + + it('returns PaymentResult with all required fields', async () => { + const result = await gateway.charge(1000, 'USD'); + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('transactionId'); + expect(result).toHaveProperty('amount'); + expect(typeof result.transactionId).toBe('string'); + }); + + it('throws InvalidCurrencyException for unsupported currency', async () => { + await expect(gateway.charge(1000, 'INVALID')) + .rejects.toThrow(InvalidCurrencyException); + }); + + it('throws TransactionNotFoundException for invalid refund', async () => { + await expect(gateway.refund('nonexistent')) + .rejects.toThrow(TransactionNotFoundException); + }); + }); +} + +// Run against all implementations +describe('StripeService', () => { + testPaymentGatewayContract(() => new StripeService(mockStripeClient)); +}); + +describe('MockPaymentService', () => { + testPaymentGatewayContract(() => new MockPaymentService()); +}); +``` + +Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle) diff --git a/.agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md b/.agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md new file mode 100644 index 000000000..c4a3274c5 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md @@ -0,0 +1,86 @@ +--- +title: Prefer Constructor Injection +impact: CRITICAL +impactDescription: Required for proper DI and testing +tags: dependency-injection, constructor, testing +--- + +## Prefer Constructor Injection + +Always use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support. + +**Incorrect (property injection with hidden dependencies):** + +```typescript +// Property injection - avoid unless necessary +@Injectable() +export class UsersService { + @Inject() + private userRepo: UserRepository; // Hidden dependency + + @Inject('CONFIG') + private config: ConfigType; // Also hidden + + async findAll() { + return this.userRepo.find(); + } +} + +// Problems: +// 1. Dependencies not visible in constructor +// 2. Service can be instantiated without dependencies in tests +// 3. TypeScript can't enforce dependency types at instantiation +``` + +**Correct (constructor injection with explicit dependencies):** + +```typescript +// Constructor injection - explicit and testable +@Injectable() +export class UsersService { + constructor( + private readonly userRepo: UserRepository, + @Inject('CONFIG') private readonly config: ConfigType, + ) {} + + async findAll(): Promise { + return this.userRepo.find(); + } +} + +// Testing is straightforward +describe('UsersService', () => { + let service: UsersService; + let mockRepo: jest.Mocked; + + beforeEach(() => { + mockRepo = { + find: jest.fn(), + save: jest.fn(), + } as any; + + service = new UsersService(mockRepo, { dbUrl: 'test' }); + }); + + it('should find all users', async () => { + mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]); + const result = await service.findAll(); + expect(result).toHaveLength(1); + }); +}); + +// Only use property injection for optional dependencies +@Injectable() +export class LoggingService { + @Optional() + @Inject('ANALYTICS') + private analytics?: AnalyticsService; + + log(message: string) { + console.log(message); + this.analytics?.track('log', message); // Optional enhancement + } +} +``` + +Reference: [NestJS Providers](https://docs.nestjs.com/providers) diff --git a/.agents/skills/nestjs-best-practices/rules/di-scope-awareness.md b/.agents/skills/nestjs-best-practices/rules/di-scope-awareness.md new file mode 100644 index 000000000..a6c77efbd --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-scope-awareness.md @@ -0,0 +1,94 @@ +--- +title: Understand Provider Scopes +impact: CRITICAL +impactDescription: Prevents data leaks and performance issues +tags: dependency-injection, scopes, request-context +--- + +## Understand Provider Scopes + +NestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing. + +**Incorrect (wrong scope usage):** + +```typescript +// Request-scoped when not needed (performance hit) +@Injectable({ scope: Scope.REQUEST }) +export class UsersService { + // This creates a new instance for EVERY request + // All dependencies also become request-scoped + async findAll() { + return this.userRepo.find(); + } +} + +// Singleton with mutable request state +@Injectable() // Default: singleton +export class RequestContextService { + private userId: string; // DANGER: Shared across all requests! + + setUser(userId: string) { + this.userId = userId; // Overwrites for all concurrent requests + } + + getUser() { + return this.userId; // Returns wrong user! + } +} +``` + +**Correct (appropriate scope for each use case):** + +```typescript +// Singleton for stateless services (default, most common) +@Injectable() +export class UsersService { + constructor(private readonly userRepo: UserRepository) {} + + async findById(id: string): Promise { + return this.userRepo.findOne({ where: { id } }); + } +} + +// Request-scoped ONLY when you need request context +@Injectable({ scope: Scope.REQUEST }) +export class RequestContextService { + private userId: string; + + setUser(userId: string) { + this.userId = userId; + } + + getUser(): string { + return this.userId; + } +} + +// Better: Use NestJS built-in request context +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; + +@Injectable({ scope: Scope.REQUEST }) +export class AuditService { + constructor(@Inject(REQUEST) private request: Request) {} + + log(action: string) { + console.log(`User ${this.request.user?.id} performed ${action}`); + } +} + +// Best: Use ClsModule for async context (no scope bubble-up) +import { ClsService } from 'nestjs-cls'; + +@Injectable() // Stays singleton! +export class AuditService { + constructor(private cls: ClsService) {} + + log(action: string) { + const userId = this.cls.get('userId'); + console.log(`User ${userId} performed ${action}`); + } +} +``` + +Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes) diff --git a/.agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md b/.agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md new file mode 100644 index 000000000..f5376a17f --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md @@ -0,0 +1,101 @@ +--- +title: Use Injection Tokens for Interfaces +impact: HIGH +impactDescription: Enables interface-based DI at runtime +tags: dependency-injection, tokens, interfaces +--- + +## Use Injection Tokens for Interfaces + +TypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments. + +**Incorrect (interface can't be used as token):** + +```typescript +// Interface can't be used as injection token +interface PaymentGateway { + charge(amount: number): Promise; +} + +@Injectable() +export class StripeService implements PaymentGateway { + charge(amount: number) { /* ... */ } +} + +@Injectable() +export class OrdersService { + // This WON'T work - PaymentGateway doesn't exist at runtime + constructor(private payment: PaymentGateway) {} +} +``` + +**Correct (symbol tokens or abstract classes):** + +```typescript +// Option 1: String/Symbol tokens (most flexible) +export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY'); + +export interface PaymentGateway { + charge(amount: number): Promise; +} + +@Injectable() +export class StripeService implements PaymentGateway { + async charge(amount: number): Promise { + // Stripe implementation + } +} + +@Injectable() +export class MockPaymentService implements PaymentGateway { + async charge(amount: number): Promise { + return { success: true, id: 'mock-id' }; + } +} + +// Module registration +@Module({ + providers: [ + { + provide: PAYMENT_GATEWAY, + useClass: process.env.NODE_ENV === 'test' + ? MockPaymentService + : StripeService, + }, + ], + exports: [PAYMENT_GATEWAY], +}) +export class PaymentModule {} + +// Injection +@Injectable() +export class OrdersService { + constructor( + @Inject(PAYMENT_GATEWAY) private payment: PaymentGateway, + ) {} + + async createOrder(dto: CreateOrderDto) { + await this.payment.charge(dto.amount); + } +} + +// Option 2: Abstract class (carries runtime type info) +export abstract class PaymentGateway { + abstract charge(amount: number): Promise; +} + +@Injectable() +export class StripeService extends PaymentGateway { + async charge(amount: number): Promise { + // Implementation + } +} + +// No @Inject needed with abstract class +@Injectable() +export class OrdersService { + constructor(private payment: PaymentGateway) {} +} +``` + +Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers) diff --git a/.agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md b/.agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md new file mode 100644 index 000000000..36c3f6af3 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md @@ -0,0 +1,125 @@ +--- +title: Handle Async Errors Properly +impact: HIGH +impactDescription: Prevents process crashes from unhandled rejections +tags: error-handling, async, promises +--- + +## Handle Async Errors Properly + +NestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net. + +**Incorrect (fire-and-forget without error handling):** + +```typescript +// Fire-and-forget without error handling +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Fire and forget - if this fails, error is unhandled! + this.emailService.sendWelcome(user.email); + + return user; + } +} + +// Unhandled promise in event handler +@Injectable() +export class OrdersService { + @OnEvent('order.created') + handleOrderCreated(event: OrderCreatedEvent) { + // This returns a promise but it's not awaited! + this.processOrder(event); + // Errors will crash the process + } + + private async processOrder(event: OrderCreatedEvent): Promise { + await this.inventoryService.reserve(event.items); + await this.notificationService.send(event.userId); + } +} + +// Missing try-catch in scheduled tasks +@Cron('0 0 * * *') +async dailyCleanup(): Promise { + await this.cleanupService.run(); + // If this throws, no error handling +} +``` + +**Correct (explicit async error handling):** + +```typescript +// Handle fire-and-forget with explicit catch +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Explicitly catch and log errors + this.emailService.sendWelcome(user.email).catch((error) => { + this.logger.error('Failed to send welcome email', error.stack); + // Optionally queue for retry + }); + + return user; + } +} + +// Properly handle async event handlers +@Injectable() +export class OrdersService { + private readonly logger = new Logger(OrdersService.name); + + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent): Promise { + try { + await this.processOrder(event); + } catch (error) { + this.logger.error('Failed to process order', { event, error }); + // Don't rethrow - would crash the process + await this.deadLetterQueue.add('order.created', event); + } + } +} + +// Safe scheduled tasks +@Injectable() +export class CleanupService { + private readonly logger = new Logger(CleanupService.name); + + @Cron('0 0 * * *') + async dailyCleanup(): Promise { + try { + await this.cleanupService.run(); + this.logger.log('Daily cleanup completed'); + } catch (error) { + this.logger.error('Daily cleanup failed', error.stack); + // Alert or retry logic + } + } +} + +// Global unhandled rejection handler in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const logger = new Logger('Bootstrap'); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + }); + + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + process.exit(1); + }); + + await app.listen(3000); +} +``` + +Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection) diff --git a/.agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md b/.agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md new file mode 100644 index 000000000..6aad9fa35 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md @@ -0,0 +1,114 @@ +--- +title: Throw HTTP Exceptions from Services +impact: HIGH +impactDescription: Keeps controllers thin and simplifies error handling +tags: error-handling, exceptions, services +--- + +## Throw HTTP Exceptions from Services + +It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes. + +**Incorrect (return error objects instead of throwing):** + +```typescript +// Return error objects instead of throwing +@Injectable() +export class UsersService { + async findById(id: string): Promise<{ user?: User; error?: string }> { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + return { error: 'User not found' }; // Controller must check this + } + return { user }; + } +} + +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string) { + const result = await this.usersService.findById(id); + if (result.error) { + throw new NotFoundException(result.error); + } + return result.user; + } +} +``` + +**Correct (throw exceptions directly from service):** + +```typescript +// Throw exceptions directly from service +@Injectable() +export class UsersService { + constructor(private readonly repo: UserRepository) {} + + async findById(id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } + + async create(dto: CreateUserDto): Promise { + const existing = await this.repo.findOne({ + where: { email: dto.email }, + }); + if (existing) { + throw new ConflictException('Email already registered'); + } + return this.repo.save(dto); + } + + async update(id: string, dto: UpdateUserDto): Promise { + const user = await this.findById(id); // Throws if not found + Object.assign(user, dto); + return this.repo.save(user); + } +} + +// Controller stays thin +@Controller('users') +export class UsersController { + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.usersService.findById(id); + } + + @Post() + create(@Body() dto: CreateUserDto): Promise { + return this.usersService.create(dto); + } +} + +// For layer-agnostic services, use domain exceptions +export class EntityNotFoundException extends Error { + constructor( + public readonly entity: string, + public readonly id: string, + ) { + super(`${entity} with ID "${id}" not found`); + } +} + +// Map to HTTP in exception filter +@Catch(EntityNotFoundException) +export class EntityNotFoundFilter implements ExceptionFilter { + catch(exception: EntityNotFoundException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + response.status(404).json({ + statusCode: 404, + message: exception.message, + entity: exception.entity, + id: exception.id, + }); + } +} +``` + +Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters) diff --git a/.agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md b/.agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md new file mode 100644 index 000000000..635823a82 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md @@ -0,0 +1,140 @@ +--- +title: Use Exception Filters for Error Handling +impact: HIGH +impactDescription: Consistent, centralized error handling +tags: error-handling, exception-filters, consistency +--- + +## Use Exception Filters for Error Handling + +Never catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions. + +**Incorrect (manual error handling in controllers):** + +```typescript +// Manual error handling in controllers +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string, @Res() res: Response) { + try { + const user = await this.usersService.findById(id); + if (!user) { + return res.status(404).json({ + statusCode: 404, + message: 'User not found', + }); + } + return res.json(user); + } catch (error) { + console.error(error); + return res.status(500).json({ + statusCode: 500, + message: 'Internal server error', + }); + } + } +} +``` + +**Correct (exception filters with consistent handling):** + +```typescript +// Use built-in and custom exceptions +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string): Promise { + const user = await this.usersService.findById(id); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } +} + +// Custom domain exception +export class UserNotFoundException extends NotFoundException { + constructor(userId: string) { + super({ + statusCode: 404, + error: 'Not Found', + message: `User with ID "${userId}" not found`, + code: 'USER_NOT_FOUND', + }); + } +} + +// Custom exception filter for domain errors +@Catch(DomainException) +export class DomainExceptionFilter implements ExceptionFilter { + catch(exception: DomainException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = exception.getStatus?.() || 400; + + response.status(status).json({ + statusCode: status, + code: exception.code, + message: exception.message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} + +// Global exception filter for unhandled errors +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + constructor(private readonly logger: Logger) {} + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.message + : 'Internal server error'; + + this.logger.error( + `${request.method} ${request.url}`, + exception instanceof Error ? exception.stack : exception, + ); + + response.status(status).json({ + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} + +// Register globally in main.ts +app.useGlobalFilters( + new AllExceptionsFilter(app.get(Logger)), + new DomainExceptionFilter(), +); + +// Or via module +@Module({ + providers: [ + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, + ], +}) +export class AppModule {} +``` + +Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters) diff --git a/.agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md b/.agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md new file mode 100644 index 000000000..0b10d926d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md @@ -0,0 +1,226 @@ +--- +title: Implement Health Checks for Microservices +impact: MEDIUM-HIGH +impactDescription: Health checks enable orchestrators to manage service lifecycle +tags: microservices, health-checks, terminus, kubernetes +--- + +## Implement Health Checks for Microservices + +Implement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly. + +**Incorrect (simple ping that doesn't check dependencies):** + +```typescript +// Simple ping that doesn't check dependencies +@Controller('health') +export class HealthController { + @Get() + check(): string { + return 'OK'; // Service might be unhealthy but returns OK + } +} + +// Health check that blocks on slow dependencies +@Controller('health') +export class HealthController { + @Get() + async check(): Promise { + // If database is slow, health check times out + await this.userRepo.findOne({ where: { id: '1' } }); + await this.redis.ping(); + await this.externalApi.healthCheck(); + return 'OK'; + } +} +``` + +**Correct (use @nestjs/terminus for comprehensive health checks):** + +```typescript +// Use @nestjs/terminus for comprehensive health checks +import { + HealthCheckService, + HttpHealthIndicator, + TypeOrmHealthIndicator, + HealthCheck, + DiskHealthIndicator, + MemoryHealthIndicator, +} from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + constructor( + private health: HealthCheckService, + private http: HttpHealthIndicator, + private db: TypeOrmHealthIndicator, + private disk: DiskHealthIndicator, + private memory: MemoryHealthIndicator, + ) {} + + // Liveness probe - is the service alive? + @Get('live') + @HealthCheck() + liveness() { + return this.health.check([ + // Basic checks only + () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB + ]); + } + + // Readiness probe - can the service handle traffic? + @Get('ready') + @HealthCheck() + readiness() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => + this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }), + () => + this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }), + ]); + } + + // Deep health check for debugging + @Get('deep') + @HealthCheck() + deepCheck() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), + () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024), + () => + this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }), + () => + this.http.pingCheck('external-api', 'https://api.example.com/health'), + ]); + } +} + +// Custom indicator for business-specific health +@Injectable() +export class QueueHealthIndicator extends HealthIndicator { + constructor(private queueService: QueueService) { + super(); + } + + async isHealthy(key: string): Promise { + const queueStats = await this.queueService.getStats(); + + const isHealthy = queueStats.failedCount < 100; + const result = this.getStatus(key, isHealthy, { + waiting: queueStats.waitingCount, + active: queueStats.activeCount, + failed: queueStats.failedCount, + }); + + if (!isHealthy) { + throw new HealthCheckError('Queue unhealthy', result); + } + + return result; + } +} + +// Redis health indicator +@Injectable() +export class RedisHealthIndicator extends HealthIndicator { + constructor(@InjectRedis() private redis: Redis) { + super(); + } + + async isHealthy(key: string): Promise { + try { + const pong = await this.redis.ping(); + return this.getStatus(key, pong === 'PONG'); + } catch (error) { + throw new HealthCheckError('Redis check failed', this.getStatus(key, false)); + } + } +} + +// Use custom indicators +@Get('ready') +@HealthCheck() +readiness() { + return this.health.check([ + () => this.db.pingCheck('database'), + () => this.redis.isHealthy('redis'), + () => this.queue.isHealthy('job-queue'), + ]); +} + +// Graceful shutdown handling +@Injectable() +export class GracefulShutdownService implements OnApplicationShutdown { + private isShuttingDown = false; + + isShutdown(): boolean { + return this.isShuttingDown; + } + + async onApplicationShutdown(signal: string): Promise { + this.isShuttingDown = true; + console.log(`Shutting down on ${signal}`); + + // Wait for in-flight requests + await new Promise((resolve) => setTimeout(resolve, 5000)); + } +} + +// Health check respects shutdown state +@Get('ready') +@HealthCheck() +readiness() { + if (this.shutdownService.isShutdown()) { + throw new ServiceUnavailableException('Shutting down'); + } + + return this.health.check([ + () => this.db.pingCheck('database'), + ]); +} +``` + +### Kubernetes Configuration + +```yaml +# Kubernetes deployment with probes +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-service +spec: + template: + spec: + containers: + - name: api + image: api-service:latest + ports: + - containerPort: 3000 + livenessProbe: + httpGet: + path: /health/live + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health/ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health/live + port: 3000 + initialDelaySeconds: 0 + periodSeconds: 5 + failureThreshold: 30 +``` + +Reference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus) diff --git a/.agents/skills/nestjs-best-practices/rules/micro-use-patterns.md b/.agents/skills/nestjs-best-practices/rules/micro-use-patterns.md new file mode 100644 index 000000000..82d7c722d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/micro-use-patterns.md @@ -0,0 +1,167 @@ +--- +title: Use Message and Event Patterns Correctly +impact: MEDIUM +impactDescription: Proper patterns ensure reliable microservice communication +tags: microservices, message-pattern, event-pattern, communication +--- + +## Use Message and Event Patterns Correctly + +NestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs. + +**Incorrect (using wrong pattern for use case):** + +```typescript +// Use @MessagePattern for fire-and-forget +@Controller() +export class NotificationsController { + @MessagePattern('user.created') + async handleUserCreated(data: UserCreatedEvent) { + // This WAITS for response, blocking the sender + await this.emailService.sendWelcome(data.email); + // If email fails, sender gets an error (coupling!) + } +} + +// Use @EventPattern expecting a response +@Controller() +export class OrdersController { + @EventPattern('inventory.check') + async checkInventory(data: CheckInventoryDto) { + const available = await this.inventory.check(data); + return available; // This return value is IGNORED with @EventPattern! + } +} + +// Tight coupling in client +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Blocks until notification service responds + await this.client.send('user.created', user).toPromise(); + // If notification service is down, user creation fails! + + return user; + } +} +``` + +**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):** + +```typescript +// MessagePattern: Request-Response (when you NEED a response) +@Controller() +export class InventoryController { + @MessagePattern({ cmd: 'check_inventory' }) + async checkInventory(data: CheckInventoryDto): Promise { + const result = await this.inventoryService.check(data.productId, data.quantity); + return result; // Response sent back to caller + } +} + +// Client expects response +@Injectable() +export class OrdersService { + async createOrder(dto: CreateOrderDto): Promise { + // Check inventory - we NEED this response to proceed + const inventory = await firstValueFrom( + this.inventoryClient.send( + { cmd: 'check_inventory' }, + { productId: dto.productId, quantity: dto.quantity }, + ), + ); + + if (!inventory.available) { + throw new BadRequestException('Insufficient inventory'); + } + + return this.repo.save(dto); + } +} + +// EventPattern: Fire-and-Forget (for notifications, side effects) +@Controller() +export class NotificationsController { + @EventPattern('user.created') + async handleUserCreated(data: UserCreatedEvent): Promise { + // No return value needed - just process the event + await this.emailService.sendWelcome(data.email); + await this.analyticsService.track('user_signup', data); + // If this fails, it doesn't affect the sender + } +} + +// Client emits event without waiting +@Injectable() +export class UsersService { + async createUser(dto: CreateUserDto): Promise { + const user = await this.repo.save(dto); + + // Fire and forget - doesn't block, doesn't wait + this.eventClient.emit('user.created', { + userId: user.id, + email: user.email, + timestamp: new Date(), + }); + + return user; // User creation succeeds regardless of event handling + } +} + +// Hybrid pattern for critical events +@Injectable() +export class OrdersService { + async createOrder(dto: CreateOrderDto): Promise { + const order = await this.repo.save(dto); + + // Critical: inventory reservation (use MessagePattern) + const reserved = await firstValueFrom( + this.inventoryClient.send({ cmd: 'reserve_inventory' }, { + orderId: order.id, + items: dto.items, + }), + ); + + if (!reserved.success) { + await this.repo.delete(order.id); + throw new BadRequestException('Could not reserve inventory'); + } + + // Non-critical: notifications (use EventPattern) + this.eventClient.emit('order.created', { + orderId: order.id, + userId: dto.userId, + total: dto.total, + }); + + return order; + } +} + +// Error handling patterns +// MessagePattern errors propagate to caller +@MessagePattern({ cmd: 'get_user' }) +async getUser(userId: string): Promise { + const user = await this.repo.findOne({ where: { id: userId } }); + if (!user) { + throw new RpcException('User not found'); // Received by caller + } + return user; +} + +// EventPattern errors should be handled locally +@EventPattern('order.created') +async handleOrderCreated(data: OrderCreatedEvent): Promise { + try { + await this.processOrder(data); + } catch (error) { + // Log and potentially retry - don't throw + this.logger.error('Failed to process order event', error); + await this.deadLetterQueue.add(data); + } +} +``` + +Reference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics) diff --git a/.agents/skills/nestjs-best-practices/rules/micro-use-queues.md b/.agents/skills/nestjs-best-practices/rules/micro-use-queues.md new file mode 100644 index 000000000..f9bc6725f --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/micro-use-queues.md @@ -0,0 +1,252 @@ +--- +title: Use Message Queues for Background Jobs +impact: MEDIUM-HIGH +impactDescription: Queues enable reliable background processing +tags: microservices, queues, bullmq, background-jobs +--- + +## Use Message Queues for Background Jobs + +Use `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests. + +**Incorrect (long-running tasks in HTTP handlers):** + +```typescript +// Long-running tasks in HTTP handlers +@Controller('reports') +export class ReportsController { + @Post() + async generate(@Body() dto: GenerateReportDto): Promise { + // This blocks the request for potentially minutes + const data = await this.fetchLargeDataset(dto); + const report = await this.processData(data); // Slow! + await this.sendEmail(dto.email, report); // Can fail! + return report; // Client times out + } +} + +// Fire-and-forget without retry +@Injectable() +export class EmailService { + async sendWelcome(email: string): Promise { + // If this fails, email is never sent + await this.mailer.send({ to: email, template: 'welcome' }); + // No retry, no tracking, no visibility + } +} + +// Use setInterval for scheduled tasks +setInterval(async () => { + await cleanupOldRecords(); +}, 60000); // No error handling, memory leaks +``` + +**Correct (use BullMQ for background processing):** + +```typescript +// Configure BullMQ +import { BullModule } from '@nestjs/bullmq'; + +@Module({ + imports: [ + BullModule.forRoot({ + connection: { + host: 'localhost', + port: 6379, + }, + defaultJobOptions: { + removeOnComplete: 1000, + removeOnFail: 5000, + attempts: 3, + backoff: { + type: 'exponential', + delay: 1000, + }, + }, + }), + BullModule.registerQueue( + { name: 'email' }, + { name: 'reports' }, + { name: 'notifications' }, + ), + ], +}) +export class QueueModule {} + +// Producer: Add jobs to queue +@Injectable() +export class ReportsService { + constructor( + @InjectQueue('reports') private reportsQueue: Queue, + ) {} + + async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> { + // Return immediately, process in background + const job = await this.reportsQueue.add('generate', dto, { + priority: dto.urgent ? 1 : 10, + delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0, + }); + + return { jobId: job.id }; + } + + async getJobStatus(jobId: string): Promise { + const job = await this.reportsQueue.getJob(jobId); + return { + status: await job.getState(), + progress: job.progress, + result: job.returnvalue, + }; + } +} + +// Consumer: Process jobs +@Processor('reports') +export class ReportsProcessor { + private readonly logger = new Logger(ReportsProcessor.name); + + @Process('generate') + async generateReport(job: Job): Promise { + this.logger.log(`Processing report job ${job.id}`); + + // Update progress + await job.updateProgress(10); + + const data = await this.fetchData(job.data); + await job.updateProgress(50); + + const report = await this.processData(data); + await job.updateProgress(90); + + await this.saveReport(report); + await job.updateProgress(100); + + return report; + } + + @OnQueueActive() + onActive(job: Job) { + this.logger.log(`Processing job ${job.id}`); + } + + @OnQueueCompleted() + onCompleted(job: Job, result: any) { + this.logger.log(`Job ${job.id} completed`); + } + + @OnQueueFailed() + onFailed(job: Job, error: Error) { + this.logger.error(`Job ${job.id} failed: ${error.message}`); + } +} + +// Email queue with retry +@Processor('email') +export class EmailProcessor { + @Process('send') + async sendEmail(job: Job): Promise { + const { to, template, data } = job.data; + + try { + await this.mailer.send({ + to, + template, + context: data, + }); + } catch (error) { + // BullMQ will retry based on job options + throw error; + } + } +} + +// Usage +@Injectable() +export class NotificationService { + constructor(@InjectQueue('email') private emailQueue: Queue) {} + + async sendWelcome(user: User): Promise { + await this.emailQueue.add( + 'send', + { + to: user.email, + template: 'welcome', + data: { name: user.name }, + }, + { + attempts: 5, + backoff: { type: 'exponential', delay: 5000 }, + }, + ); + } +} + +// Scheduled jobs +@Injectable() +export class ScheduledJobsService implements OnModuleInit { + constructor(@InjectQueue('maintenance') private queue: Queue) {} + + async onModuleInit(): Promise { + // Clean up old reports daily at midnight + await this.queue.add( + 'cleanup', + {}, + { + repeat: { cron: '0 0 * * *' }, + jobId: 'daily-cleanup', // Prevent duplicates + }, + ); + + // Send digest every hour + await this.queue.add( + 'digest', + {}, + { + repeat: { every: 60 * 60 * 1000 }, + jobId: 'hourly-digest', + }, + ); + } +} + +@Processor('maintenance') +export class MaintenanceProcessor { + @Process('cleanup') + async cleanup(): Promise { + await this.cleanupOldReports(); + await this.cleanupExpiredSessions(); + } + + @Process('digest') + async sendDigest(): Promise { + const users = await this.getUsersForDigest(); + for (const user of users) { + await this.emailQueue.add('send', { to: user.email, template: 'digest' }); + } + } +} + +// Queue monitoring with Bull Board +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; + +@Module({ + imports: [ + BullBoardModule.forRoot({ + route: '/admin/queues', + adapter: ExpressAdapter, + }), + BullBoardModule.forFeature({ + name: 'email', + adapter: BullMQAdapter, + }), + BullBoardModule.forFeature({ + name: 'reports', + adapter: BullMQAdapter, + }), + ], +}) +export class AdminModule {} +``` + +Reference: [NestJS Queues](https://docs.nestjs.com/techniques/queues) diff --git a/.agents/skills/nestjs-best-practices/rules/perf-async-hooks.md b/.agents/skills/nestjs-best-practices/rules/perf-async-hooks.md new file mode 100644 index 000000000..7ca007710 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/perf-async-hooks.md @@ -0,0 +1,109 @@ +--- +title: Use Async Lifecycle Hooks Correctly +impact: HIGH +impactDescription: Improper async handling blocks application startup +tags: performance, lifecycle, async, hooks +--- + +## Use Async Lifecycle Hooks Correctly + +NestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately. + +**Incorrect (fire-and-forget async without await):** + +```typescript +// Fire-and-forget async without await +@Injectable() +export class DatabaseService implements OnModuleInit { + onModuleInit() { + // This runs but doesn't block - app starts before DB is ready! + this.connect(); + } + + private async connect() { + await this.pool.connect(); + console.log('Database connected'); + } +} + +// Heavy blocking operations in constructor +@Injectable() +export class ConfigService { + private config: Config; + + constructor() { + // BLOCKS entire module instantiation synchronously + this.config = fs.readFileSync('config.json'); + } +} +``` + +**Correct (return promises from async hooks):** + +```typescript +// Return promise from async hooks +@Injectable() +export class DatabaseService implements OnModuleInit { + private pool: Pool; + + async onModuleInit(): Promise { + // NestJS waits for this to complete before continuing + await this.pool.connect(); + console.log('Database connected'); + } + + async onModuleDestroy(): Promise { + // Clean up resources on shutdown + await this.pool.end(); + console.log('Database disconnected'); + } +} + +// Use onApplicationBootstrap for cross-module dependencies +@Injectable() +export class CacheWarmerService implements OnApplicationBootstrap { + constructor( + private cache: CacheService, + private products: ProductsService, + ) {} + + async onApplicationBootstrap(): Promise { + // All modules are initialized, safe to warm cache + const products = await this.products.findPopular(); + await this.cache.warmup(products); + } +} + +// Heavy init in async hooks, not constructor +@Injectable() +export class ConfigService implements OnModuleInit { + private config: Config; + + constructor() { + // Keep constructor synchronous and fast + } + + async onModuleInit(): Promise { + // Async loading in lifecycle hook + this.config = await this.loadConfig(); + } + + private async loadConfig(): Promise { + const file = await fs.promises.readFile('config.json'); + return JSON.parse(file.toString()); + } + + get(key: string): T { + return this.config[key]; + } +} + +// Enable shutdown hooks in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling + await app.listen(3000); +} +``` + +Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events) diff --git a/.agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md b/.agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md new file mode 100644 index 000000000..8bcc5828c --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md @@ -0,0 +1,121 @@ +--- +title: Use Lazy Loading for Large Modules +impact: MEDIUM +impactDescription: Improves startup time for large applications +tags: performance, lazy-loading, modules, optimization +--- + +## Use Lazy Loading for Large Modules + +NestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs. + +**Incorrect (loading everything eagerly):** + +```typescript +// Load everything eagerly in a large app +@Module({ + imports: [ + UsersModule, + OrdersModule, + PaymentsModule, + ReportsModule, // Heavy, rarely used + AnalyticsModule, // Heavy, rarely used + AdminModule, // Only admins use this + LegacyModule, // Migration module, rarely used + BulkImportModule, // Used once a month + ], +}) +export class AppModule {} + +// All modules initialize at startup, even if never used +// Slow cold starts in serverless +// Memory wasted on unused modules +``` + +**Correct (lazy load rarely-used modules):** + +```typescript +// Use LazyModuleLoader for optional modules +import { LazyModuleLoader } from '@nestjs/core'; + +@Injectable() +export class ReportsService { + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async generateReport(type: string): Promise { + // Load module only when needed + const { ReportsModule } = await import('./reports/reports.module'); + const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule); + + const reportsService = moduleRef.get(ReportsGeneratorService); + return reportsService.generate(type); + } +} + +// Lazy load admin features with caching +@Injectable() +export class AdminService { + private adminModule: ModuleRef | null = null; + + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + private async getAdminModule(): Promise { + if (!this.adminModule) { + const { AdminModule } = await import('./admin/admin.module'); + this.adminModule = await this.lazyModuleLoader.load(() => AdminModule); + } + return this.adminModule; + } + + async runAdminTask(task: string): Promise { + const moduleRef = await this.getAdminModule(); + const taskRunner = moduleRef.get(AdminTaskRunner); + await taskRunner.run(task); + } +} + +// Reusable lazy loader service +@Injectable() +export class ModuleLoaderService { + private loadedModules = new Map(); + + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async load( + key: string, + importFn: () => Promise<{ default: Type } | Type>, + ): Promise { + if (!this.loadedModules.has(key)) { + const module = await importFn(); + const moduleType = 'default' in module ? module.default : module; + const moduleRef = await this.lazyModuleLoader.load(() => moduleType); + this.loadedModules.set(key, moduleRef); + } + return this.loadedModules.get(key)!; + } +} + +// Preload modules in background after startup +@Injectable() +export class ModulePreloader implements OnApplicationBootstrap { + constructor(private lazyModuleLoader: LazyModuleLoader) {} + + async onApplicationBootstrap(): Promise { + setTimeout(async () => { + await this.preloadModule(() => import('./reports/reports.module')); + }, 5000); // 5 seconds after startup + } + + private async preloadModule(importFn: () => Promise): Promise { + try { + const module = await importFn(); + const moduleType = module.default || Object.values(module)[0]; + await this.lazyModuleLoader.load(() => moduleType); + } catch (error) { + console.warn('Failed to preload module', error); + } + } +} +``` + +Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules) diff --git a/.agents/skills/nestjs-best-practices/rules/perf-optimize-database.md b/.agents/skills/nestjs-best-practices/rules/perf-optimize-database.md new file mode 100644 index 000000000..964189f7d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/perf-optimize-database.md @@ -0,0 +1,131 @@ +--- +title: Optimize Database Queries +impact: HIGH +impactDescription: Database queries are typically the largest source of latency +tags: performance, database, queries, optimization +--- + +## Optimize Database Queries + +Select only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries. + +**Incorrect (over-fetching data and missing indexes):** + +```typescript +// Select everything when you need few fields +@Injectable() +export class UsersService { + async findAllEmails(): Promise { + const users = await this.repo.find(); + // Fetches ALL columns for ALL users + return users.map((u) => u.email); + } + + async getUserSummary(id: string): Promise { + const user = await this.repo.findOne({ + where: { id }, + relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'], + }); + // Over-fetches massive relation tree + return { name: user.name, postCount: user.posts.length }; + } +} + +// No indexes on frequently queried columns +@Entity() +export class Order { + @Column() + userId: string; // No index - full table scan on every lookup + + @Column() + status: string; // No index - slow status filtering +} +``` + +**Correct (select only needed data with proper indexes):** + +```typescript +// Select only needed columns +@Injectable() +export class UsersService { + async findAllEmails(): Promise { + const users = await this.repo.find({ + select: ['email'], // Only fetch email column + }); + return users.map((u) => u.email); + } + + // Use QueryBuilder for complex selections + async getUserSummary(id: string): Promise { + return this.repo + .createQueryBuilder('user') + .select('user.name', 'name') + .addSelect('COUNT(post.id)', 'postCount') + .leftJoin('user.posts', 'post') + .where('user.id = :id', { id }) + .groupBy('user.id') + .getRawOne(); + } + + // Fetch relations only when needed + async getFullProfile(id: string): Promise { + return this.repo.findOne({ + where: { id }, + relations: ['posts'], // Only immediate relation + select: { + id: true, + name: true, + email: true, + posts: { + id: true, + title: true, + }, + }, + }); + } +} + +// Add indexes on frequently queried columns +@Entity() +@Index(['userId']) +@Index(['status']) +@Index(['createdAt']) +@Index(['userId', 'status']) // Composite index for common query pattern +export class Order { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column() + status: string; + + @CreateDateColumn() + createdAt: Date; +} + +// Always paginate large datasets +@Injectable() +export class OrdersService { + async findAll(page = 1, limit = 20): Promise> { + const [items, total] = await this.repo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + order: { createdAt: 'DESC' }, + }); + + return { + items, + meta: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }; + } +} +``` + +Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder) diff --git a/.agents/skills/nestjs-best-practices/rules/perf-use-caching.md b/.agents/skills/nestjs-best-practices/rules/perf-use-caching.md new file mode 100644 index 000000000..71706907b --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/perf-use-caching.md @@ -0,0 +1,128 @@ +--- +title: Use Caching Strategically +impact: HIGH +impactDescription: Dramatically reduces database load and response times +tags: performance, caching, redis, optimization +--- + +## Use Caching Strategically + +Implement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas. + +**Incorrect (no caching or caching everything):** + +```typescript +// No caching for expensive, repeated queries +@Injectable() +export class ProductsService { + async getPopular(): Promise { + // Runs complex aggregation query EVERY request + return this.productsRepo + .createQueryBuilder('p') + .leftJoin('p.orders', 'o') + .select('p.*, COUNT(o.id) as orderCount') + .groupBy('p.id') + .orderBy('orderCount', 'DESC') + .limit(20) + .getMany(); + } +} + +// Cache everything without thought +@Injectable() +export class UsersService { + @CacheKey('users') + @CacheTTL(3600) + @UseInterceptors(CacheInterceptor) + async findAll(): Promise { + // Caching user list for 1 hour is wrong if data changes frequently + return this.usersRepo.find(); + } +} +``` + +**Correct (strategic caching with proper invalidation):** + +```typescript +// Setup caching module +@Module({ + imports: [ + CacheModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + stores: [ + new KeyvRedis(config.get('REDIS_URL')), + ], + ttl: 60 * 1000, // Default 60s + }), + }), + ], +}) +export class AppModule {} + +// Manual caching for granular control +@Injectable() +export class ProductsService { + constructor( + @Inject(CACHE_MANAGER) private cache: Cache, + private productsRepo: ProductRepository, + ) {} + + async getPopular(): Promise { + const cacheKey = 'products:popular'; + + // Try cache first + const cached = await this.cache.get(cacheKey); + if (cached) return cached; + + // Cache miss - fetch and cache + const products = await this.fetchPopularProducts(); + await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL + return products; + } + + // Invalidate cache on changes + async updateProduct(id: string, dto: UpdateProductDto): Promise { + const product = await this.productsRepo.save({ id, ...dto }); + await this.cache.del('products:popular'); // Invalidate + return product; + } +} + +// Decorator-based caching with auto-interceptor +@Controller('categories') +@UseInterceptors(CacheInterceptor) +export class CategoriesController { + @Get() + @CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change + findAll(): Promise { + return this.categoriesService.findAll(); + } + + @Get(':id') + @CacheTTL(60 * 1000) // 1 minute + @CacheKey('category') + findOne(@Param('id') id: string): Promise { + return this.categoriesService.findOne(id); + } +} + +// Event-based cache invalidation +@Injectable() +export class CacheInvalidationService { + constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} + + @OnEvent('product.created') + @OnEvent('product.updated') + @OnEvent('product.deleted') + async invalidateProductCaches(event: ProductEvent) { + await Promise.all([ + this.cache.del('products:popular'), + this.cache.del(`product:${event.productId}`), + ]); + } +} +``` + +Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching) diff --git a/.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md b/.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md new file mode 100644 index 000000000..a0d1ff031 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md @@ -0,0 +1,146 @@ +--- +title: Implement Secure JWT Authentication +impact: CRITICAL +impactDescription: Essential for secure APIs +tags: security, jwt, authentication, tokens +--- + +## Implement Secure JWT Authentication + +Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads. + +**Incorrect (insecure JWT implementation):** + +```typescript +// Hardcode secrets +@Module({ + imports: [ + JwtModule.register({ + secret: 'my-secret-key', // Exposed in code + signOptions: { expiresIn: '7d' }, // Too long + }), + ], +}) +export class AuthModule {} + +// Store sensitive data in JWT +async login(user: User): Promise<{ accessToken: string }> { + const payload = { + sub: user.id, + email: user.email, + password: user.password, // NEVER include password! + ssn: user.ssn, // NEVER include sensitive data! + isAdmin: user.isAdmin, // Can be tampered if not verified + }; + return { accessToken: this.jwtService.sign(payload) }; +} + +// Skip token validation +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: 'my-secret', + }); + } + + async validate(payload: any): Promise { + return payload; // No validation of user existence + } +} +``` + +**Correct (secure JWT with refresh tokens):** + +```typescript +// Secure JWT configuration +@Module({ + imports: [ + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { + expiresIn: '15m', // Short-lived access tokens + issuer: config.get('JWT_ISSUER'), + audience: config.get('JWT_AUDIENCE'), + }, + }), + }), + PassportModule.register({ defaultStrategy: 'jwt' }), + ], +}) +export class AuthModule {} + +// Minimal JWT payload +@Injectable() +export class AuthService { + async login(user: User): Promise { + // Only include necessary, non-sensitive data + const payload: JwtPayload = { + sub: user.id, + email: user.email, + roles: user.roles, + iat: Math.floor(Date.now() / 1000), + }; + + const accessToken = this.jwtService.sign(payload); + const refreshToken = await this.createRefreshToken(user.id); + + return { accessToken, refreshToken, expiresIn: 900 }; + } + + private async createRefreshToken(userId: string): Promise { + const token = randomBytes(32).toString('hex'); + const hashedToken = await bcrypt.hash(token, 10); + + await this.refreshTokenRepo.save({ + userId, + token: hashedToken, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }); + + return token; + } +} + +// Proper JWT strategy with validation +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private config: ConfigService, + private usersService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.get('JWT_SECRET'), + ignoreExpiration: false, + issuer: config.get('JWT_ISSUER'), + audience: config.get('JWT_AUDIENCE'), + }); + } + + async validate(payload: JwtPayload): Promise { + // Verify user still exists and is active + const user = await this.usersService.findById(payload.sub); + + if (!user || !user.isActive) { + throw new UnauthorizedException('User not found or inactive'); + } + + // Verify token wasn't issued before password change + if (user.passwordChangedAt) { + const tokenIssuedAt = new Date(payload.iat * 1000); + if (tokenIssuedAt < user.passwordChangedAt) { + throw new UnauthorizedException('Token invalidated by password change'); + } + } + + return user; + } +} +``` + +Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication) diff --git a/.agents/skills/nestjs-best-practices/rules/security-rate-limiting.md b/.agents/skills/nestjs-best-practices/rules/security-rate-limiting.md new file mode 100644 index 000000000..7d39e9c8c --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/security-rate-limiting.md @@ -0,0 +1,125 @@ +--- +title: Implement Rate Limiting +impact: HIGH +impactDescription: Protects against abuse and ensures fair resource usage +tags: security, rate-limiting, throttler, protection +--- + +## Implement Rate Limiting + +Use `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments. + +**Incorrect (no rate limiting on sensitive endpoints):** + +```typescript +// No rate limiting on sensitive endpoints +@Controller('auth') +export class AuthController { + @Post('login') + async login(@Body() dto: LoginDto): Promise { + // Attackers can brute-force credentials + return this.authService.login(dto); + } + + @Post('forgot-password') + async forgotPassword(@Body() dto: ForgotPasswordDto): Promise { + // Can be abused to spam users with emails + return this.authService.sendResetEmail(dto.email); + } +} + +// Same limits for all endpoints +@UseGuards(ThrottlerGuard) +@Controller('api') +export class ApiController { + @Get('public-data') + async getPublic() {} // Should allow more requests + + @Post('process-payment') + async payment() {} // Should be more restrictive +} +``` + +**Correct (configured throttler with endpoint-specific limits):** + +```typescript +// Configure throttler globally with multiple limits +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; + +@Module({ + imports: [ + ThrottlerModule.forRoot([ + { + name: 'short', + ttl: 1000, // 1 second + limit: 3, // 3 requests per second + }, + { + name: 'medium', + ttl: 10000, // 10 seconds + limit: 20, // 20 requests per 10 seconds + }, + { + name: 'long', + ttl: 60000, // 1 minute + limit: 100, // 100 requests per minute + }, + ]), + ], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], +}) +export class AppModule {} + +// Override limits per endpoint +@Controller('auth') +export class AuthController { + @Post('login') + @Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute + async login(@Body() dto: LoginDto): Promise { + return this.authService.login(dto); + } + + @Post('forgot-password') + @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour + async forgotPassword(@Body() dto: ForgotPasswordDto): Promise { + return this.authService.sendResetEmail(dto.email); + } +} + +// Skip throttling for certain routes +@Controller('health') +export class HealthController { + @Get() + @SkipThrottle() + check(): string { + return 'OK'; + } +} + +// Custom throttle per user type +@Injectable() +export class CustomThrottlerGuard extends ThrottlerGuard { + protected async getTracker(req: Request): Promise { + // Use user ID if authenticated, IP otherwise + return req.user?.id || req.ip; + } + + protected async getLimit(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + // Higher limits for authenticated users + if (request.user) { + return request.user.isPremium ? 1000 : 200; + } + + return 50; // Anonymous users + } +} +``` + +Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting) diff --git a/.agents/skills/nestjs-best-practices/rules/security-sanitize-output.md b/.agents/skills/nestjs-best-practices/rules/security-sanitize-output.md new file mode 100644 index 000000000..78e3d396a --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/security-sanitize-output.md @@ -0,0 +1,139 @@ +--- +title: Sanitize Output to Prevent XSS +impact: HIGH +impactDescription: XSS vulnerabilities can compromise user sessions and data +tags: security, xss, sanitization, html +--- + +## Sanitize Output to Prevent XSS + +While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers. + +**Incorrect (storing raw HTML without sanitization):** + +```typescript +// Store raw HTML from users +@Injectable() +export class CommentsService { + async create(dto: CreateCommentDto): Promise { + // User can inject: + return this.repo.save({ + content: dto.content, // Raw, unsanitized + authorId: dto.authorId, + }); + } +} + +// Return HTML without sanitization +@Controller('pages') +export class PagesController { + @Get(':slug') + @Header('Content-Type', 'text/html') + async getPage(@Param('slug') slug: string): Promise { + const page = await this.pagesService.findBySlug(slug); + // If page.content contains user input, XSS is possible + return `${page.content}`; + } +} + +// Reflect user input in errors +@Get(':id') +async findOne(@Param('id') id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + // XSS if id contains malicious content and error is rendered + throw new NotFoundException(`User ${id} not found`); + } + return user; +} +``` + +**Correct (sanitize content and use proper headers):** + +```typescript +// Sanitize HTML content before storage +import * as sanitizeHtml from 'sanitize-html'; + +@Injectable() +export class CommentsService { + private readonly sanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], + allowedAttributes: { + a: ['href', 'title'], + }, + allowedSchemes: ['http', 'https', 'mailto'], + }; + + async create(dto: CreateCommentDto): Promise { + return this.repo.save({ + content: sanitizeHtml(dto.content, this.sanitizeOptions), + authorId: dto.authorId, + }); + } +} + +// Use validation pipe to strip HTML +import { Transform } from 'class-transformer'; + +export class CreatePostDto { + @IsString() + @MaxLength(1000) + @Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] })) + title: string; + + @IsString() + @Transform(({ value }) => + sanitizeHtml(value, { + allowedTags: ['p', 'br', 'b', 'i', 'a'], + allowedAttributes: { a: ['href'] }, + }), + ) + content: string; +} + +// Set proper Content-Type headers +@Controller('api') +export class ApiController { + @Get('data') + @Header('Content-Type', 'application/json') + async getData(): Promise { + // JSON response - browser won't execute scripts + return this.service.getData(); + } +} + +// Sanitize error messages +@Get(':id') +async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { + const user = await this.repo.findOne({ where: { id } }); + if (!user) { + // UUID validation ensures safe format + throw new NotFoundException('User not found'); + } + return user; +} + +// Use Helmet for CSP headers +import helmet from 'helmet'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https:'], + }, + }, + }), + ); + + await app.listen(3000); +} +``` + +Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) diff --git a/.agents/skills/nestjs-best-practices/rules/security-use-guards.md b/.agents/skills/nestjs-best-practices/rules/security-use-guards.md new file mode 100644 index 000000000..fb1359c4a --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/security-use-guards.md @@ -0,0 +1,135 @@ +--- +title: Use Guards for Authentication and Authorization +impact: HIGH +impactDescription: Enforces access control before handlers execute +tags: security, guards, authentication, authorization +--- + +## Use Guards for Authentication and Authorization + +Guards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers. + +**Incorrect (manual auth checks in every handler):** + +```typescript +// Manual auth checks in every handler +@Controller('admin') +export class AdminController { + @Get('users') + async getUsers(@Request() req) { + if (!req.user) { + throw new UnauthorizedException(); + } + if (!req.user.roles.includes('admin')) { + throw new ForbiddenException(); + } + return this.adminService.getUsers(); + } + + @Delete('users/:id') + async deleteUser(@Request() req, @Param('id') id: string) { + if (!req.user) { + throw new UnauthorizedException(); + } + if (!req.user.roles.includes('admin')) { + throw new ForbiddenException(); + } + return this.adminService.deleteUser(id); + } +} +``` + +**Correct (guards with declarative decorators):** + +```typescript +// JWT Auth Guard +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private reflector: Reflector, + ) {} + + async canActivate(context: ExecutionContext): Promise { + // Check for @Public() decorator + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + const request = context.switchToHttp().getRequest(); + const token = this.extractToken(request); + + if (!token) { + throw new UnauthorizedException('No token provided'); + } + + try { + request.user = await this.jwtService.verifyAsync(token); + return true; + } catch { + throw new UnauthorizedException('Invalid token'); + } + } + + private extractToken(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} + +// Roles Guard +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride('roles', [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) return true; + + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.some((role) => user.roles?.includes(role)); + } +} + +// Decorators +export const Public = () => SetMetadata('isPublic', true); +export const Roles = (...roles: Role[]) => SetMetadata('roles', roles); + +// Register guards globally +@Module({ + providers: [ + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: RolesGuard }, + ], +}) +export class AppModule {} + +// Clean controller +@Controller('admin') +@Roles(Role.Admin) // Applied to all routes +export class AdminController { + @Get('users') + getUsers(): Promise { + return this.adminService.getUsers(); + } + + @Delete('users/:id') + deleteUser(@Param('id') id: string): Promise { + return this.adminService.deleteUser(id); + } + + @Public() // Override: no auth required + @Get('health') + health() { + return { status: 'ok' }; + } +} +``` + +Reference: [NestJS Guards](https://docs.nestjs.com/guards) diff --git a/.agents/skills/nestjs-best-practices/rules/security-validate-all-input.md b/.agents/skills/nestjs-best-practices/rules/security-validate-all-input.md new file mode 100644 index 000000000..f489d064e --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/security-validate-all-input.md @@ -0,0 +1,150 @@ +--- +title: Validate All Input with DTOs and Pipes +impact: HIGH +impactDescription: First line of defense against attacks +tags: security, validation, dto, pipes +--- + +## Validate All Input with DTOs and Pipes + +Always validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing. + +**Incorrect (trust raw input without validation):** + +```typescript +// Trust raw input without validation +@Controller('users') +export class UsersController { + @Post() + create(@Body() body: any) { + // body could contain anything - SQL injection, XSS, etc. + return this.usersService.create(body); + } + + @Get() + findAll(@Query() query: any) { + // query.limit could be "'; DROP TABLE users; --" + return this.usersService.findAll(query.limit); + } +} + +// DTOs without validation decorators +export class CreateUserDto { + name: string; // No validation + email: string; // Could be "not-an-email" + age: number; // Could be "abc" or -999 +} +``` + +**Correct (validated DTOs with global ValidationPipe):** + +```typescript +// Enable ValidationPipe globally in main.ts +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strip unknown properties + forbidNonWhitelisted: true, // Throw on unknown properties + transform: true, // Auto-transform to DTO types + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + + await app.listen(3000); +} + +// Create well-validated DTOs +import { + IsString, + IsEmail, + IsInt, + Min, + Max, + IsOptional, + MinLength, + MaxLength, + Matches, + IsNotEmpty, +} from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +export class CreateUserDto { + @IsString() + @IsNotEmpty() + @MinLength(2) + @MaxLength(100) + @Transform(({ value }) => value?.trim()) + name: string; + + @IsEmail() + @Transform(({ value }) => value?.toLowerCase().trim()) + email: string; + + @IsInt() + @Min(0) + @Max(150) + age: number; + + @IsString() + @MinLength(8) + @MaxLength(100) + @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { + message: 'Password must contain uppercase, lowercase, and number', + }) + password: string; +} + +// Query DTO with defaults and transformation +export class FindUsersQueryDto { + @IsOptional() + @IsString() + @MaxLength(100) + search?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit: number = 20; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset: number = 0; +} + +// Param validation +export class UserIdParamDto { + @IsUUID('4') + id: string; +} + +@Controller('users') +export class UsersController { + @Post() + create(@Body() dto: CreateUserDto): Promise { + // dto is guaranteed to be valid + return this.usersService.create(dto); + } + + @Get() + findAll(@Query() query: FindUsersQueryDto): Promise { + // query.limit is a number, query.search is sanitized + return this.usersService.findAll(query); + } + + @Get(':id') + findOne(@Param() params: UserIdParamDto): Promise { + // params.id is a valid UUID + return this.usersService.findById(params.id); + } +} +``` + +Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation) diff --git a/.agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md b/.agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md new file mode 100644 index 000000000..426551385 --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md @@ -0,0 +1,178 @@ +--- +title: Use Supertest for E2E Testing +impact: HIGH +impactDescription: Validates the full request/response cycle +tags: testing, e2e, supertest, integration +--- + +## Use Supertest for E2E Testing + +End-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss. + +**Incorrect (no proper E2E setup or teardown):** + +```typescript +// Only unit test controllers +describe('UsersController', () => { + it('should return users', async () => { + const service = { findAll: jest.fn().mockResolvedValue([]) }; + const controller = new UsersController(service as any); + + const result = await controller.findAll(); + + expect(result).toEqual([]); + // Doesn't test: routes, guards, pipes, serialization + }); +}); + +// E2E tests without proper setup/teardown +describe('Users API', () => { + it('should create user', async () => { + const app = await NestFactory.create(AppModule); + // No proper initialization + // No cleanup after test + // Hits real database + }); +}); +``` + +**Correct (proper E2E setup with Supertest):** + +```typescript +// Proper E2E test setup +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('UsersController (e2e)', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + + // Apply same config as production + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('/users (POST)', () => { + it('should create a user', () => { + return request(app.getHttpServer()) + .post('/users') + .send({ name: 'John', email: 'john@test.com' }) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id'); + expect(res.body.name).toBe('John'); + expect(res.body.email).toBe('john@test.com'); + }); + }); + + it('should return 400 for invalid email', () => { + return request(app.getHttpServer()) + .post('/users') + .send({ name: 'John', email: 'invalid-email' }) + .expect(400) + .expect((res) => { + expect(res.body.message).toContain('email'); + }); + }); + }); + + describe('/users/:id (GET)', () => { + it('should return 404 for non-existent user', () => { + return request(app.getHttpServer()) + .get('/users/non-existent-id') + .expect(404); + }); + }); +}); + +// Testing with authentication +describe('Protected Routes (e2e)', () => { + let app: INestApplication; + let authToken: string; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + await app.init(); + + // Get auth token + const loginResponse = await request(app.getHttpServer()) + .post('/auth/login') + .send({ email: 'test@test.com', password: 'password' }); + + authToken = loginResponse.body.accessToken; + }); + + it('should return 401 without token', () => { + return request(app.getHttpServer()) + .get('/users/me') + .expect(401); + }); + + it('should return user profile with valid token', () => { + return request(app.getHttpServer()) + .get('/users/me') + .set('Authorization', `Bearer ${authToken}`) + .expect(200) + .expect((res) => { + expect(res.body.email).toBe('test@test.com'); + }); + }); +}); + +// Database isolation for E2E tests +describe('Orders API (e2e)', () => { + let app: INestApplication; + let dataSource: DataSource; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: '.env.test', // Test database config + }), + AppModule, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + dataSource = moduleFixture.get(DataSource); + await app.init(); + }); + + beforeEach(async () => { + // Clean database between tests + await dataSource.synchronize(true); + }); + + afterAll(async () => { + await dataSource.destroy(); + await app.close(); + }); +}); +``` + +Reference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing) diff --git a/.agents/skills/nestjs-best-practices/rules/test-mock-external-services.md b/.agents/skills/nestjs-best-practices/rules/test-mock-external-services.md new file mode 100644 index 000000000..e29b595ad --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/test-mock-external-services.md @@ -0,0 +1,179 @@ +--- +title: Mock External Services in Tests +impact: HIGH +impactDescription: Ensures fast, reliable, deterministic tests +tags: testing, mocking, external-services, jest +--- + +## Mock External Services in Tests + +Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors. + +**Incorrect (calling real APIs and databases):** + +```typescript +// Call real APIs in tests +describe('PaymentService', () => { + it('should process payment', async () => { + const service = new PaymentService(new StripeClient(realApiKey)); + // Hits real Stripe API! + const result = await service.charge('tok_visa', 1000); + // Slow, costs money, flaky + }); +}); + +// Use real database +describe('UsersService', () => { + beforeEach(async () => { + await connection.query('DELETE FROM users'); // Modifies real DB + }); + + it('should create user', async () => { + const user = await service.create({ email: 'test@test.com' }); + // Side effects on shared database + }); +}); + +// Incomplete mocks +const mockHttpService = { + get: jest.fn().mockResolvedValue({ data: {} }), + // Missing error scenarios, missing other methods +}; +``` + +**Correct (mock all external dependencies):** + +```typescript +// Mock HTTP service properly +describe('WeatherService', () => { + let service: WeatherService; + let httpService: jest.Mocked; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + WeatherService, + { + provide: HttpService, + useValue: { + get: jest.fn(), + post: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(WeatherService); + httpService = module.get(HttpService); + }); + + it('should return weather data', async () => { + const mockResponse = { + data: { temperature: 72, humidity: 45 }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + + httpService.get.mockReturnValue(of(mockResponse)); + + const result = await service.getWeather('NYC'); + + expect(result).toEqual({ temperature: 72, humidity: 45 }); + }); + + it('should handle API timeout', async () => { + httpService.get.mockReturnValue( + throwError(() => new Error('ETIMEDOUT')), + ); + + await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable'); + }); + + it('should handle rate limiting', async () => { + httpService.get.mockReturnValue( + throwError(() => ({ + response: { status: 429, data: { message: 'Rate limited' } }, + })), + ); + + await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException); + }); +}); + +// Mock repository instead of database +describe('UsersService', () => { + let service: UsersService; + let repo: jest.Mocked>; + + beforeEach(async () => { + const mockRepo = { + find: jest.fn(), + findOne: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + createQueryBuilder: jest.fn(), + }; + + const module = await Test.createTestingModule({ + providers: [ + UsersService, + { provide: getRepositoryToken(User), useValue: mockRepo }, + ], + }).compile(); + + service = module.get(UsersService); + repo = module.get(getRepositoryToken(User)); + }); + + it('should find user by id', async () => { + const mockUser = { id: '1', name: 'John', email: 'john@test.com' }; + repo.findOne.mockResolvedValue(mockUser); + + const result = await service.findById('1'); + + expect(result).toEqual(mockUser); + expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + }); +}); + +// Create mock factory for complex SDKs +function createMockStripe(): jest.Mocked { + return { + paymentIntents: { + create: jest.fn(), + retrieve: jest.fn(), + confirm: jest.fn(), + cancel: jest.fn(), + }, + customers: { + create: jest.fn(), + retrieve: jest.fn(), + }, + } as any; +} + +// Mock time for time-dependent tests +describe('TokenService', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-15')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should expire token after 1 hour', async () => { + const token = await service.createToken(); + + // Fast-forward time + jest.advanceTimersByTime(61 * 60 * 1000); + + expect(await service.isValid(token)).toBe(false); + }); +}); +``` + +Reference: [Jest Mocking](https://jestjs.io/docs/mock-functions) diff --git a/.agents/skills/nestjs-best-practices/rules/test-use-testing-module.md b/.agents/skills/nestjs-best-practices/rules/test-use-testing-module.md new file mode 100644 index 000000000..de256f57d --- /dev/null +++ b/.agents/skills/nestjs-best-practices/rules/test-use-testing-module.md @@ -0,0 +1,153 @@ +--- +title: Use Testing Module for Unit Tests +impact: HIGH +impactDescription: Enables proper isolated testing with mocked dependencies +tags: testing, unit-tests, mocking, jest +--- + +## Use Testing Module for Unit Tests + +Use `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation. + +**Incorrect (manual instantiation bypassing DI):** + +```typescript +// Instantiate services manually without DI +describe('UsersService', () => { + it('should create user', async () => { + // Manual instantiation bypasses DI + const repo = new UserRepository(); // Real repo! + const service = new UsersService(repo); + + const user = await service.create({ name: 'Test' }); + // This hits the real database! + }); +}); + +// Test implementation details +describe('UsersController', () => { + it('should call service', async () => { + const service = { create: jest.fn() }; + const controller = new UsersController(service as any); + + await controller.create({ name: 'Test' }); + + expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior + }); +}); +``` + +**Correct (use Test.createTestingModule with mocked dependencies):** + +```typescript +// Use Test.createTestingModule for proper DI +import { Test, TestingModule } from '@nestjs/testing'; + +describe('UsersService', () => { + let service: UsersService; + let repo: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UsersService, + { + provide: UserRepository, + useValue: { + save: jest.fn(), + findOne: jest.fn(), + find: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(UsersService); + repo = module.get(UserRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should save and return user', async () => { + const dto = { name: 'John', email: 'john@test.com' }; + const expectedUser = { id: '1', ...dto }; + + repo.save.mockResolvedValue(expectedUser); + + const result = await service.create(dto); + + expect(result).toEqual(expectedUser); + expect(repo.save).toHaveBeenCalledWith(dto); + }); + + it('should throw on duplicate email', async () => { + repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' }); + + await expect( + service.create({ name: 'Test', email: 'test@test.com' }), + ).rejects.toThrow(ConflictException); + }); + }); + + describe('findById', () => { + it('should return user when found', async () => { + const user = { id: '1', name: 'John' }; + repo.findOne.mockResolvedValue(user); + + const result = await service.findById('1'); + + expect(result).toEqual(user); + }); + + it('should throw NotFoundException when not found', async () => { + repo.findOne.mockResolvedValue(null); + + await expect(service.findById('999')).rejects.toThrow(NotFoundException); + }); + }); +}); + +// Testing guards and interceptors +describe('RolesGuard', () => { + let guard: RolesGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [RolesGuard, Reflector], + }).compile(); + + guard = module.get(RolesGuard); + reflector = module.get(Reflector); + }); + + it('should allow when no roles required', () => { + const context = createMockExecutionContext({ user: { roles: [] } }); + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined); + + expect(guard.canActivate(context)).toBe(true); + }); + + it('should allow admin for admin-only route', () => { + const context = createMockExecutionContext({ user: { roles: ['admin'] } }); + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); + + expect(guard.canActivate(context)).toBe(true); + }); +}); + +function createMockExecutionContext(request: Partial): ExecutionContext { + return { + switchToHttp: () => ({ + getRequest: () => request, + }), + getHandler: () => jest.fn(), + getClass: () => jest.fn(), + } as ExecutionContext; +} +``` + +Reference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing) diff --git a/.claude/skills/nestjs-best-practices b/.claude/skills/nestjs-best-practices new file mode 120000 index 000000000..ed0955adb --- /dev/null +++ b/.claude/skills/nestjs-best-practices @@ -0,0 +1 @@ +../../.agents/skills/nestjs-best-practices \ No newline at end of file diff --git a/.config/prisma.ts b/.config/prisma.ts index a9e7bbf64..4556ba305 100644 --- a/.config/prisma.ts +++ b/.config/prisma.ts @@ -7,7 +7,7 @@ expand(config({ quiet: true })); export default defineConfig({ datasource: { - url: process.env.DATABASE_URL ?? '' + url: process.env.DIRECT_URL ?? process.env.DATABASE_URL }, migrations: { path: join(__dirname, '..', 'prisma', 'migrations'), diff --git a/.gitignore b/.gitignore index 0f5a1651e..ab31ae269 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ npm-debug.log .env.prod .github/instructions/nx.instructions.md .nx/cache +.nx/polygraph +.nx/self-healing .nx/workspace-data /.sass-cache /connect.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..7253a5cee --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=7 diff --git a/.prettierignore b/.prettierignore index 5524c76f6..ff97b3cbc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,7 @@ /.agents/skills/angular-developer +/.agents/skills/nestjs-best-practices /.nx/cache +/.nx/self-healing /.nx/workspace-data /apps/client/src/polyfills.ts /dist diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d0be119..553d54e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,80 @@ 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). +## 3.5.0 - 2026-05-24 + +### Added + +- Configured the `min-release-age` in `.npmrc` + +### Changed + +- Removed the deprecated attributes (`assetClass`, `countries`, `currency`, `dataSource`, `name`, `sectors`, `symbol` and `url`) from the holdings of the public portfolio endpoint response +- Removed the deprecated `api/v1/order` endpoints +- Upgraded `@keyv/redis` from version `4.4.0` to `5.1.6` + +### Fixed + +- Fixed a layout regression that caused a double scrollbar on pages without tabs +- Resolved an issue with missing cash positions caused by an incorrect data source + +## 3.4.0 - 2026-05-21 + +### Added + +- Added the icon column to the benchmark component +- Added support for the `DIRECT_URL` environment variable to enable direct database connections + +### Changed + +- Improved the pagination in the activities table of the account detail dialog +- Improved the pagination in the activities table of the holding detail dialog +- Randomized the placeholder in the assistant +- Filtered out sectors with zero weight for ETF and mutual fund assets in the _Yahoo Finance_ data enhancer +- Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental) +- Improved the verification of the _Stripe_ checkout session when creating a subscription +- Relaxed the URL validation in the asset profile DTOs to accept both `HTTP` and `HTTPS` protocols +- Relaxed the URL validation in the platform DTOs to accept both `HTTP` and `HTTPS` protocols +- Extracted the page tabs to a reusable component +- Improved the language localization for German (`de`) +- Improved the language localization for Spanish (`es`) +- Upgraded `bull-board` from version `7.0.0` to `7.1.5` +- Upgraded `Nx` from version `22.7.1` to `22.7.2` + +### Fixed + +- Resolved an issue with the cash balance calculation of an account for `SELL` activities to ensure fees are correctly subtracted +- Resolved an exception in the portfolio details endpoint when an asset profile is unmatched + +## 3.3.0 - 2026-05-14 + +### Added + +- Added `nestjs-best-practices` skills + +### Changed + +- Deactivated asset profiles automatically on delisting in the _Financial Modeling Prep_ service +- Migrated various components from `NgClass` to class bindings +- Refreshed the cryptocurrencies list +- Improved the language localization for Spanish (`es`) +- Cleaned up the _Webpack Bundle Analyzer_ setup +- Upgraded `@internationalized/number` from version `3.6.5` to `3.6.6` +- Upgraded `@ionic/angular` from version `8.8.1` to `8.8.5` +- Upgraded `@openrouter/ai-sdk-provider` from version `0.7.2` to `2.9.0` +- Upgraded `ai` from version `4.3.16` to `6.0.174` +- Upgraded `bull-board` from version `6.20.3` to `7.0.0` +- Upgraded `countries-and-timezones` from version `3.8.0` to `3.9.0` +- Upgraded `fuse.js` from version `7.1.0` to `7.3.0` +- Upgraded `Nx` from version `22.6.5` to `22.7.1` +- Upgraded `papaparse` from version `5.3.1` to `5.5.3` +- Upgraded `prisma` from version `7.7.0` to `7.8.0` + +### Fixed + +- Synchronized the native browser elements with the theme to improve the dark mode +- Fixed a visual regression in the bottom navigation bar on mobile + ## 3.2.0 - 2026-05-03 ### Added diff --git a/README.md b/README.md index 87710d610..8557d4330 100644 --- a/README.md +++ b/README.md @@ -85,26 +85,27 @@ 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, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | -| `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. | #### OpenID Connect OIDC (Experimental) diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 6b0440dc4..e847f600d 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -43,11 +43,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { ActivitiesService } from './activities.service'; -@Controller([ - 'activities', - /** @deprecated */ - 'order' -]) +@Controller('activities') export class ActivitiesController { public constructor( private readonly activitiesService: ActivitiesService, diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 58b9c11a4..821185e11 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -214,19 +214,18 @@ export class ActivitiesService { }); if (updateAccountBalance === true) { - let amount = new Big(data.unitPrice) - .mul(data.quantity) - .plus(data.fee) - .toNumber(); + let amount = new Big(data.unitPrice).mul(data.quantity); if (['BUY', 'FEE'].includes(data.type)) { - amount = new Big(amount).mul(-1).toNumber(); + amount = amount.mul(-1); } + amount = amount.minus(data.fee); + await this.accountService.updateAccountBalance({ accountId, - amount, userId, + amount: amount.toNumber(), currency: data.SymbolProfile.connectOrCreate.create.currency, date: data.date as Date }); diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 3316f9ce4..4857c7e14 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -74,29 +74,25 @@ import { UserModule } from './user/user.module'; AuthDeviceModule, AuthModule, BenchmarksModule, - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forRoot({ - adapter: ExpressAdapter, - boardOptions: { - uiConfig: { - boardLogo: { - height: 0, - path: '', - width: 0 - }, - boardTitle: 'Job Queues', - favIcon: { - alternative: '/assets/favicon-32x32.png', - default: '/assets/favicon-32x32.png' - } - } - }, - middleware: BullBoardAuthMiddleware, - route: BULL_BOARD_ROUTE - }) - ] - : []), + BullBoardModule.forRoot({ + adapter: ExpressAdapter, + boardOptions: { + uiConfig: { + boardLogo: { + height: 0, + path: '', + width: 0 + }, + boardTitle: 'Job Queues', + favIcon: { + alternative: '/assets/favicon-32x32.png', + default: '/assets/favicon-32x32.png' + } + } + }, + middleware: BullBoardAuthMiddleware, + route: BULL_BOARD_ROUTE + }), BullModule.forRoot({ redis: { db: parseInt(process.env.REDIS_DB ?? '0', 10), diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index eab4ecf8b..5267f40c8 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -28,6 +28,7 @@ import { AiService } from './ai.service'; @Module({ controllers: [AiController], + exports: [AiService], imports: [ ActivitiesModule, ApiModule, diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index d07768d69..d0ef17844 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -1,4 +1,5 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PROPERTY_API_KEY_OPENROUTER, @@ -36,11 +37,18 @@ export class AiService { ]; public constructor( + private readonly configurationService: ConfigurationService, private readonly portfolioService: PortfolioService, private readonly propertyService: PropertyService ) {} - public async generateText({ prompt }: { prompt: string }) { + public async generateText({ + prompt, + requestTimeout = this.configurationService.get('REQUEST_TIMEOUT') + }: { + prompt: string; + requestTimeout?: number; + }) { const openRouterApiKey = await this.propertyService.getByKey( PROPERTY_API_KEY_OPENROUTER ); @@ -55,7 +63,8 @@ export class AiService { return generateText({ prompt, - model: openRouterService.chat(openRouterModel) + model: openRouterService.chat(openRouterModel), + timeout: requestTimeout }); } @@ -92,11 +101,13 @@ export class AiService { .map( ({ allocationInPercentage, - assetClass, - assetSubClass, - currency, - name: label, - symbol + assetProfile: { + assetClass, + assetSubClass, + currency, + name: label, + symbol + } }) => { return AiService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.reduce( (row, { key, name }) => { diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts index 970925777..74bb6b672 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts @@ -5,7 +5,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { + DEFAULT_DATE_RANGE, + HEADER_KEY_IMPERSONATION +} from '@ghostfolio/common/config'; import type { AssetProfileIdentifier, BenchmarkMarketDataDetailsResponse, @@ -118,7 +121,7 @@ export class BenchmarksController { @Param('dataSource') dataSource: DataSource, @Param('startDateString') startDateString: string, @Param('symbol') symbol: string, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 33c6052a6..08e49704b 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -21,7 +21,11 @@ import { UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { Type as ActivityType } from '@prisma/client'; +import { + AssetClass, + AssetSubClass, + Type as ActivityType +} from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -151,11 +155,11 @@ export class PublicController { }; const totalValue = getSum( - Object.values(holdings).map(({ currency, marketPrice, quantity }) => { + Object.values(holdings).map(({ assetProfile, marketPrice, quantity }) => { return new Big( this.exchangeRateDataService.toCurrency( quantity * marketPrice, - currency, + assetProfile.currency, this.request.user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY ) @@ -167,19 +171,41 @@ export class PublicController { publicPortfolioResponse.holdings[symbol] = { allocationInPercentage: portfolioPosition.valueInBaseCurrency / totalValue, - assetClass: hasDetails ? portfolioPosition.assetClass : undefined, - assetProfile: hasDetails ? portfolioPosition.assetProfile : undefined, - countries: hasDetails ? portfolioPosition.countries : [], - currency: hasDetails ? portfolioPosition.currency : undefined, - dataSource: portfolioPosition.dataSource, + assetProfile: { + ...portfolioPosition.assetProfile, + assetClass: + hasDetails || + portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY + ? portfolioPosition.assetProfile.assetClass + : undefined, + assetClassLabel: + hasDetails || + portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY + ? portfolioPosition.assetProfile.assetClassLabel + : undefined, + assetSubClass: + hasDetails || + portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH + ? portfolioPosition.assetProfile.assetSubClass + : undefined, + assetSubClassLabel: + hasDetails || + portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH + ? portfolioPosition.assetProfile.assetSubClassLabel + : undefined, + ...(hasDetails + ? {} + : { + countries: [], + currency: undefined, + holdings: [], + sectors: [] + }) + }, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, markets: hasDetails ? portfolioPosition.markets : undefined, - name: portfolioPosition.name, netPerformancePercentWithCurrencyEffect: portfolioPosition.netPerformancePercentWithCurrencyEffect, - sectors: hasDetails ? portfolioPosition.sectors : [], - symbol: portfolioPosition.symbol, - url: portfolioPosition.url, valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue }; } diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts index 5542ae933..35f3fa348 100644 --- a/apps/api/src/app/health/health.controller.ts +++ b/apps/api/src/app/health/health.controller.ts @@ -1,5 +1,7 @@ +import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { + AiServiceHealthResponse, DataEnhancerHealthResponse, DataProviderHealthResponse } from '@ghostfolio/common/interfaces'; @@ -9,6 +11,7 @@ import { Get, HttpException, HttpStatus, + Logger, Param, Res, UseInterceptors @@ -21,7 +24,10 @@ import { HealthService } from './health.service'; @Controller('health') export class HealthController { - public constructor(private readonly healthService: HealthService) {} + public constructor( + private readonly aiService: AiService, + private readonly healthService: HealthService + ) {} @Get() public async getHealth(@Res() response: Response) { @@ -40,6 +46,29 @@ export class HealthController { } } + @Get('ai') + public async getHealthOfAiService( + @Res() response: Response + ): Promise> { + try { + const { text } = await this.aiService.generateText({ + prompt: `Reply with the word "OK" and nothing else.` + }); + + if (text === 'OK') { + return response + .status(HttpStatus.OK) + .json({ status: getReasonPhrase(StatusCodes.OK) }); + } + } catch (error) { + Logger.error(error, 'HealthController'); + } + + return response + .status(HttpStatus.SERVICE_UNAVAILABLE) + .json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) }); + } + @Get('data-enhancer/:name') public async getHealthOfDataEnhancer( @Param('name') name: string, diff --git a/apps/api/src/app/health/health.module.ts b/apps/api/src/app/health/health.module.ts index b8c4d5810..c36924121 100644 --- a/apps/api/src/app/health/health.module.ts +++ b/apps/api/src/app/health/health.module.ts @@ -1,3 +1,4 @@ +import { AiModule } from '@ghostfolio/api/app/endpoints/ai/ai.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module'; @@ -12,6 +13,7 @@ import { HealthService } from './health.service'; @Module({ controllers: [HealthController], imports: [ + AiModule, DataEnhancerModule, DataProviderModule, PropertyModule, diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index b454b01cd..f0a451975 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -2,7 +2,10 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; -import { resetHours } from '@ghostfolio/common/helper'; +import { + getAssetProfileIdentifier, + resetHours +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, DataProviderInfo, @@ -114,8 +117,8 @@ export class CurrentRateService { errors: quoteErrors.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }), - values: uniqBy(values, ({ date, symbol }) => { - return `${date}-${symbol}`; + values: uniqBy(values, ({ dataSource, date, symbol }) => { + return `${date}-${getAssetProfileIdentifier({ dataSource, symbol })}`; }) }; @@ -124,7 +127,11 @@ export class CurrentRateService { try { // If missing quote, fallback to the latest available historical market price let value: GetValueObject = response.values.find((currentValue) => { - return currentValue.symbol === symbol && isToday(currentValue.date); + return ( + currentValue.dataSource === dataSource && + currentValue.symbol === symbol && + isToday(currentValue.date) + ); }); if (!value) { @@ -147,7 +154,11 @@ export class CurrentRateService { const [latestValue] = response.values .filter((currentValue) => { - return currentValue.symbol === symbol && currentValue.marketPrice; + return ( + currentValue.dataSource === dataSource && + currentValue.marketPrice && + currentValue.symbol === symbol + ); }) .sort((a, b) => { if (a.date < b.date) { diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index cb6f21e8a..8aa94ee92 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -14,6 +14,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { + DEFAULT_DATE_RANGE, HEADER_KEY_IMPERSONATION, UNKNOWN_KEY } from '@ghostfolio/common/config'; @@ -82,7 +83,7 @@ export class PortfolioController { @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string, @Query('withMarkets') withMarketsParam = 'false' @@ -220,6 +221,21 @@ export class PortfolioController { hasDetails || portfolioPosition.assetClass === AssetClass.LIQUIDITY ? portfolioPosition.assetClass : undefined, + assetProfile: { + ...portfolioPosition.assetProfile, + ...(hasDetails + ? {} + : { + assetClass: undefined, + assetClassLabel: undefined, + assetSubClass: undefined, + assetSubClassLabel: undefined, + countries: [], + currency: undefined, + holdings: [], + sectors: [] + }) + }, assetSubClass: hasDetails || portfolioPosition.assetSubClass === AssetSubClass.CASH ? portfolioPosition.assetSubClass @@ -306,7 +322,7 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('groupBy') groupBy?: GroupBy, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string ): Promise { @@ -407,7 +423,7 @@ export class PortfolioController { @Query('dataSource') filterByDataSource?: string, @Query('holdingType') filterByHoldingType?: string, @Query('query') filterBySearchQuery?: string, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string ): Promise { @@ -440,7 +456,7 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, @Query('groupBy') groupBy?: GroupBy, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string ): Promise { @@ -512,7 +528,7 @@ export class PortfolioController { @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE, @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string, @Query('withExcludedAccounts') withExcludedAccountsParam = 'false' diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 60b413cf9..cee36ec27 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -32,11 +32,17 @@ import { } from '@ghostfolio/common/calculation-helper'; import { DEFAULT_CURRENCY, + DEFAULT_DATE_RANGE, TAG_ID_EMERGENCY_FUND, TAG_ID_EXCLUDE_FROM_ANALYSIS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + getAssetProfileIdentifier, + getSum, + parseDate +} from '@ghostfolio/common/helper'; import { AccountsResponse, Activity, @@ -64,7 +70,7 @@ import { } from '@ghostfolio/common/types'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Account, @@ -465,7 +471,7 @@ export class PortfolioService { } public async getDetails({ - dateRange = 'max', + dateRange = DEFAULT_DATE_RANGE, filters, impersonationId, userId, @@ -558,9 +564,17 @@ export class PortfolioService { const cashSymbolProfiles = this.getCashSymbolProfiles(cashDetails); symbolProfiles.push(...cashSymbolProfiles); - const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; + const symbolProfileMap: { + [assetProfileIdentifier: string]: EnhancedSymbolProfile; + } = {}; + for (const symbolProfile of symbolProfiles) { - symbolProfileMap[symbolProfile.symbol] = symbolProfile; + symbolProfileMap[ + getAssetProfileIdentifier({ + dataSource: symbolProfile.dataSource, + symbol: symbolProfile.symbol + }) + ] = symbolProfile; } const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; @@ -571,6 +585,7 @@ export class PortfolioService { for (const { activitiesCount, currency, + dataSource, dateOfFirstActivity, dividend, grossPerformance, @@ -600,7 +615,17 @@ export class PortfolioService { } } - const assetProfile = symbolProfileMap[symbol]; + const assetProfile = + symbolProfileMap[getAssetProfileIdentifier({ dataSource, symbol })]; + + if (!assetProfile) { + Logger.warn( + `Asset profile not found for ${symbol} (${dataSource})`, + 'PortfolioService' + ); + + continue; + } let markets: PortfolioPosition['markets']; let marketsAdvanced: PortfolioPosition['marketsAdvanced']; @@ -989,7 +1014,7 @@ export class PortfolioService { } public async getPerformance({ - dateRange = 'max', + dateRange = DEFAULT_DATE_RANGE, filters, impersonationId, userId @@ -1587,7 +1612,7 @@ export class PortfolioService { assetSubClass: AssetSubClass.CASH, countries: [], createdAt: account.createdAt, - dataSource: DataSource.MANUAL, + dataSource: this.dataProviderService.getDataSourceForExchangeRates(), holdings: [], id: currency, isActive: true, diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index e1c705fdd..3e6316ec6 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -100,10 +100,12 @@ export class SubscriptionController { request.query.checkoutSessionId as string ); - Logger.log( - `Subscription for user '${userId}' has been created via Stripe`, - 'SubscriptionController' - ); + if (userId) { + Logger.log( + `Subscription for user '${userId}' has been created via Stripe`, + 'SubscriptionController' + ); + } response.redirect( `${this.configurationService.get( diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 07795d0d1..557d81976 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -17,7 +17,7 @@ import { } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { Subscription } from '@prisma/client'; +import { Prisma, Subscription } from '@prisma/client'; import { addMilliseconds, isBefore } from 'date-fns'; import ms, { StringValue } from 'ms'; import Stripe from 'stripe'; @@ -108,11 +108,13 @@ export class SubscriptionService { duration = '1 year', durationExtension, price, + stripeCheckoutSessionId, userId }: { duration?: StringValue; durationExtension?: StringValue; price: number; + stripeCheckoutSessionId?: string; userId: string; }) { let expiresAt = addMilliseconds(new Date(), ms(duration)); @@ -125,6 +127,7 @@ export class SubscriptionService { data: { expiresAt, price, + stripeCheckoutSessionId, user: { connect: { id: userId @@ -136,25 +139,42 @@ export class SubscriptionService { public async createSubscriptionViaStripe(aCheckoutSessionId: string) { try { - let durationExtension: StringValue; - const session = await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId); + if (session.payment_status !== 'paid' || session.status !== 'complete') { + throw new Error( + `Stripe Checkout Session '${aCheckoutSessionId}' has not been paid (status=${session.status}, payment_status=${session.payment_status})` + ); + } + const subscriptionOffer: SubscriptionOffer = JSON.parse( session.metadata.subscriptionOffer ?? '{}' ); - if (subscriptionOffer) { - durationExtension = subscriptionOffer.durationExtension; + const durationExtension = subscriptionOffer?.durationExtension; + + try { + await this.createSubscription({ + durationExtension, + price: session.amount_total / 100, + stripeCheckoutSessionId: session.id, + userId: session.client_reference_id + }); + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + Logger.log( + `Stripe Checkout Session '${session.id}' has already been redeemed`, + 'SubscriptionService' + ); + } else { + throw error; + } } - await this.createSubscription({ - durationExtension, - price: session.amount_total / 100, - userId: session.client_reference_id - }); - return session.client_reference_id; } catch (error) { Logger.error(error, 'SubscriptionService'); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 64efb161f..278566691 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -26,6 +26,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DEFAULT_CURRENCY, + DEFAULT_DATE_RANGE, DEFAULT_LANGUAGE_CODE, PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SYSTEM_MESSAGE, @@ -290,7 +291,8 @@ export class UserService { (user.settings.settings as UserSettings).dateRange = (user.settings.settings as UserSettings).viewMode === 'ZEN' ? 'max' - : ((user.settings.settings as UserSettings)?.dateRange ?? 'max'); + : ((user.settings.settings as UserSettings)?.dateRange ?? + DEFAULT_DATE_RANGE); // Set default value for performance calculation type if (!(user.settings.settings as UserSettings)?.performanceCalculationType) { @@ -541,7 +543,7 @@ export class UserService { } if (hasRole(user, Role.ADMIN)) { - if (this.configurationService.get('ENABLE_FEATURE_BULL_BOARD')) { + if ((user.settings.settings as UserSettings).isExperimentalFeatures) { currentPermissions.push(permissions.accessAdminControlBullBoard); } diff --git a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json index 7ca2d568f..ea88dd4c1 100644 --- a/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json +++ b/apps/api/src/assets/cryptocurrencies/cryptocurrencies.json @@ -193,6 +193,7 @@ "AARK": "Aark", "AART": "ALL.ART", "AAST": "AASToken", + "AASTEROID": "Alien Asteroid", "AAT": "Agricultural Trade Chain", "AAVAWBTC": "Aave aWBTC", "AAVE": "Aave", @@ -280,7 +281,7 @@ "ACTA": "Acta Finance", "ACTIN": "Actinium", "ACTN": "Action Coin", - "ACU": "ACU Platform", + "ACU": "Acurast Token", "ACX": "Across Protocol", "ACXT": "ACDX Exchange Token", "ACYC": "All Coins Yield Capital", @@ -389,7 +390,7 @@ "AGATA": "Agatech", "AGATOKEN": "AGA Token", "AGB": "Apes Go Bananas", - "AGC": "Argocoin", + "AGC": "Alien Green Cat", "AGEN": "Agent Krasnov", "AGENT": "AgentLayer", "AGENTFUN": "AgentFun.AI", @@ -417,6 +418,7 @@ "AGOV": "Answer Governance", "AGPC": "AGPC", "AGRI": "AgriDex Token", + "AGRICULTURALUNIONS": "Agricultural Unions", "AGRO": "Bit Agro", "AGRS": "Agoras Token", "AGS": "Aegis", @@ -605,6 +607,7 @@ "ALBART": "Albärt", "ALBE": "ALBETROS", "ALBEDO": "ALBEDO", + "ALBON": "Albemarle (Ondo Tokenized)", "ALBT": "AllianceBlock", "ALC": "Arab League Coin", "ALCAZAR": "Alcazar", @@ -700,6 +703,7 @@ "ALTMAN": "SAM", "ALTOCAR": "AltoCar", "ALTR": "Altranium", + "ALTSZN": "ALTSEASON", "ALTT": "Altcoinist", "ALU": "Altura", "ALUSD": "Alchemix USD", @@ -994,6 +998,7 @@ "ARG": "Argentine Football Association Fan Token", "ARGENTUM": "Argentum", "ARGO": "ArGoApp", + "ARGOCOIN": "Argocoin", "ARGON": "Argon", "ARGUS": "ArgusCoin", "ARI": "AriCoin", @@ -1123,10 +1128,11 @@ "ASTA": "ASTA", "ASTER": "Aster", "ASTERINU": "Aster INU", - "ASTEROID": "ASTEROID", + "ASTEROID": "Asteroid Shiba", "ASTEROIDBOT": "Asteroid Bot", "ASTEROIDCOIN": "ASTEROID", "ASTEROIDETH": "Asteroid", + "ASTEROIDFIT": "ASTEROID", "ASTHERUSUSDF": "Astherus USDF", "ASTO": "Altered State Token", "ASTON": "Aston", @@ -1396,7 +1402,6 @@ "BABI": "Babylons", "BABL": "Babylon Finance", "BABY": "Babylon", - "BABY4": "Baby 4", "BABYANDY": "Baby Andy", "BABYASTER": "Baby Aster", "BABYB": "Baby Bali", @@ -1778,7 +1783,7 @@ "BDID": "BDID", "BDIN": "BendDAO BDIN", "BDL": "Bitdeal", - "BDOG": "Bulldog Token", + "BDOG": "BurnDog", "BDOGITO": "BullDogito", "BDOT": "Binance Wrapped DOT", "BDP": "Big Data Protocol", @@ -1846,6 +1851,7 @@ "BELA": "Bela", "BELG": "Belgian Malinois", "BELIEVE": "Believe", + "BELKA": "The Dancing Squirrel", "BELL": "Bellscoin", "BELLE": "Isabelle", "BELLS": "Bellscoin", @@ -1882,8 +1888,9 @@ "BERN": "BERNcash", "BERNIE": "BERNIE SENDERS", "BERRIE": "Berrie Token", - "BERRY": "Berry", + "BERRY": "Strawberry AI", "BERRYS": "BerrySwap", + "BERRYSTORE": "Berry", "BERT": "Bertram The Pomeranian", "BES": "battle esports coin", "BESA": "Besa Gaming", @@ -1935,6 +1942,7 @@ "BGB": "Bitget token", "BGBG": "BigMouthFrog", "BGBP": "Binance GBP Stable Coin", + "BGBTC": "Bitget Wrapped BTC", "BGBV1": "Bitget Token v1", "BGC": "Bee Token", "BGCI": "Bloomberg Galaxy Crypto Index", @@ -1989,6 +1997,7 @@ "BIDI": "Bidipass", "BIDP": "BID Protocol", "BIDR": "Binance IDR Stable Coin", + "BIDUON": "Baidu (Ondo Tokenized)", "BIDZ": "BIDZ Coin", "BIDZV1": "BIDZ Coin v1", "BIFI": "Beefy.Finance", @@ -2133,6 +2142,7 @@ "BITTO": "BITTO", "BITTON": "Bitton", "BITTY": "The Bitcoin Mascot", + "BITUBU": "UBU", "BITUPTOKEN": "BitUP Token", "BITUSD": "bitUSD", "BITV": "Bitvolt", @@ -2196,6 +2206,7 @@ "BLC": "BlakeCoin", "BLCT": "Bloomzed Loyalty Club Ticket", "BLD": "Agoric", + "BLEND": "Fluent", "BLENDR": "Blendr Network", "BLEPE": "Blepe", "BLERF": "BLERF", @@ -2218,7 +2229,7 @@ "BLKC": "BlackHat Coin", "BLKD": "Blinked", "BLKS": "Blockshipping", - "BLM": "Blombard", + "BLM": "BLM coin", "BLN": "Bulleon", "BLNM": "Bolenum", "BLOB": "B.O.B the Blob", @@ -2247,6 +2258,7 @@ "BLOCX": "BLOCX.", "BLOGGE": "Bloggercube", "BLOK": "Bloktopia", + "BLOMBARD": "Blombard", "BLOO": "bloo foster coin", "BLOOCYS": "BlooCYS", "BLOODY": "Bloody Token", @@ -2880,6 +2892,7 @@ "BTFA": "Banana Task Force Ape", "BTG": "Bitcoin Gold", "BTGON": "B2Gold (Ondo Tokenized)", + "BTGOON": "BitGo Holdings (Ondo Tokenized)", "BTH": "Bithereum", "BTK": "Bostoken", "BTL": "Bitlocus", @@ -2973,11 +2986,12 @@ "BUL": "bul", "BULDAK": "Buldak", "BULEI": "Bulei", - "BULL": "Tron Bull", + "BULL": "Bull", "BULLA": "BULLA", "BULLBEAR": "BullBear AI", "BULLC": "BuySell", "BULLDOG": "BullDog Coin", + "BULLDOGTOKEN": "Bulldog Token", "BULLF": "BULL FINANCE", "BULLGOD": "Bull God", "BULLI": "Bullish On Ethereum", @@ -3392,7 +3406,6 @@ "CDOGE": "cyberdoge", "CDPT": "Creditor Data Platform", "CDRX": "CDRX", - "CDT": "CheckDot", "CDX": "CDX Network", "CDY": "Bitcoin Candy", "CDragon": "Clumsy Dragon", @@ -3513,6 +3526,7 @@ "CHC": "ChainCoin", "CHD": "CharityDAO", "CHECK": "Checkmate", + "CHECKDOT": "CheckDot", "CHECKR": "CheckerChain", "CHECOIN": "CheCoin", "CHED": "Giggleched", @@ -3573,6 +3587,7 @@ "CHINU": "Chubby Inu", "CHIP": "Chip", "CHIPI": "chipi", + "CHIPP": "Chip", "CHIPPY": "Chippy", "CHIPS": "CHIPS", "CHIRP": "Chirp Token", @@ -4350,6 +4365,7 @@ "CVX": "Convex Finance", "CVXCRV": "Convex CRV", "CVXFXS": "Convex FXS", + "CVXON": "Chevron (Ondo Tokenized)", "CVXX": "Chevron xStock", "CW": "CardWallet", "CWA": "Chris World Asset", @@ -4741,6 +4757,7 @@ "DEPAY": "DePay", "DEPIN": "DEPIN", "DEPINU": "Depression Inu", + "DEPLOYR": "Deployr", "DEPO": "Depo", "DEPTH": "Depth Token", "DEQ": "Dequant", @@ -5363,6 +5380,7 @@ "DUA": "Brillion", "DUAL": "DUAL", "DUALDAOTOKEN": "Dual Finance", + "DUALV1": "BLOCKv", "DUB": "DubCoin", "DUBAICAT": "Dubai Cat", "DUBBZ": "Dubbz", @@ -5548,6 +5566,7 @@ "ECHO": "Echo", "ECHOBOT": "ECHO BOT", "ECHOD": "EchoDEX", + "ECHON": "iShares MSCI Chile ETF (Ondo Tokenized)", "ECHT": "e-Chat", "ECI": "Euro Cup Inu", "ECKODAO": "eckoDAO", @@ -6110,6 +6129,7 @@ "EVERGROW": "EverGrowCoin", "EVERLIFE": "EverLife.AI", "EVERMOON": "EverMoon", + "EVERRISE": "EverRise", "EVERV": "EverValue Coin", "EVERY": "Everyworld", "EVIL": "EvilCoin", @@ -6160,6 +6180,7 @@ "EXN": "Exeno", "EXNT": "EXNT", "EXO": "Exosis", + "EXODON": "Exodus Movement (Ondo Tokenized)", "EXOS": "Exobots", "EXP": "Expanse", "EXPAND": "Gems", @@ -6213,6 +6234,7 @@ "FACTR": "Defactor", "FACTRPAY": "FactR", "FACY": "ArAIstotle Fact Checker", + "FADEWALLET": "FadeWallet Token", "FADO": "FADO Go", "FAFO": "FAFO", "FAFOSOL": "Fafo", @@ -6312,6 +6334,7 @@ "FCT": "FirmaChain", "FCTC": "FaucetCoin", "FCTR": "FactorDAO", + "FCXON": "Freeport-McMoRan (Ondo Tokenized)", "FDC": "FDrive Coin", "FDGC": "FINTECH DIGITAL GOLD COIN", "FDLS": "FIDELIS", @@ -6364,6 +6387,7 @@ "FFCT": "FortFC", "FFM": "Files.fm Library", "FFN": "Fairy Forest", + "FFOGON": "Franklin Focused Growth ETF (Ondo Tokenized)", "FFTP": "FIGHT FOR THE PEOPLE", "FFUEL": "getFIFO", "FFYI": "Fiscus FYI", @@ -6535,6 +6559,7 @@ "FLOOR": "FloorDAO", "FLOP": "Big Floppa", "FLOPPA": "Floppa Cat", + "FLOR": "FLORK", "FLORK": "FLORK BNB", "FLORKY": "Florky", "FLOSHIDO": "FLOSHIDO INU", @@ -6549,6 +6574,7 @@ "FLOWP": "Flow Protocol", "FLOYX": "Floyx", "FLP": "Gameflip", + "FLQLON": "Franklin US Large Cap Multifactor Index ETF (Ondo Tokenized)", "FLR": "Flare", "FLRBRG": "Floor Cheese Burger", "FLRS": "Flourish Coin", @@ -6802,6 +6828,7 @@ "FSN": "Fusion", "FSNV1": "Fusion v1", "FSO": "FSociety", + "FSOLON": "Fidelity Solana Fund (Ondo Tokenized)", "FST": "FreeStyle Token", "FSTC": "FastCoin", "FSTR": "Fourth Star", @@ -7131,6 +7158,7 @@ "GENS": "Genshiro", "GENSLR": "Good Gensler", "GENSTAKE": "Genstake", + "GENSYN": "Gensyn", "GENT": "Gentleman", "GENX": "Genx Token", "GENXNET": "Genesis Network", @@ -7313,6 +7341,7 @@ "GLR": "Glory Finance", "GLS": "Glacier", "GLT": "GlobalToken", + "GLTRON": "abrdn Physical Precious Metals Basket Shares ETF (Ondo Tokenized)", "GLUE": "Glue", "GLX": "GalaxyCoin", "GLYPH": "GlyphCoin", @@ -7569,10 +7598,11 @@ "GRIDZ": "GridZone.io", "GRIFFAIN": "GRIFFAIN", "GRIFT": "ORBIT", - "GRIM": "GRIMREAPER", + "GRIM": "GrimHustle", "GRIMACE": "Grimace", "GRIMEVO": "Grim EVO", "GRIMEX": "SpaceGrime", + "GRIMREAPER": "GRIMREAPER", "GRIN": "Grin", "GRIND": "Self Improving", "GRIPPY": "GRIPPY", @@ -7955,6 +7985,7 @@ "HEWE": "Health & Wealth", "HEX": "HEX", "HEXC": "HexCoin", + "HEYFLORK": "HeyFlork", "HEZ": "Hermez Network Token", "HF": "Have Fun", "HFI": "Holder Finance", @@ -8348,11 +8379,12 @@ "IBG": "iBG Token", "IBGT": "Infrared BGT", "IBIT": "InfinityBit Token", + "IBITON": "iShares Bitcoin Trust (Ondo Tokenized)", "IBMX": "International Business Machines xStock", "IBNB": "iBNB", "IBP": "Innovation Blockchain Payment", "IBS": "Irbis Network", - "IC": "Ignition", + "IC": "Icy", "ICA": "Icarus Network", "ICAP": "ICAP Token", "ICASH": "ICASH", @@ -8442,6 +8474,7 @@ "IGGT": "The Invincible Game Token", "IGI": "Igi", "IGNIS": "Ignis", + "IGNITION": "Ignition", "IGT": "Infinitar", "IGTT": "IGT", "IGU": "IguVerse", @@ -8496,7 +8529,6 @@ "IMS": "Independent Money System", "IMST": "Imsmart", "IMT": "Immortal Token", - "IMU": "Immunefi", "IMUSIFY": "imusify", "IMVR": "ImmVRse", "IMX": "Immutable X", @@ -8509,11 +8541,13 @@ "INCEPT": "Incept", "INCNT": "Incent", "INCO": "InfinitiCoin", + "INCOME": "Universal High Income", "INCORGNITO": "Incorgnito", "INCP": "InceptionCoin", "INCREMENTUM": "Incrementum", "INCX": "INCX Coin", "IND": "Indorse", + "INDAON": "iShares MSCI India ETF (Ondo Tokenized)", "INDAY": "Independence Day", "INDEPENDENCEDAY": "Independence Day", "INDEX": "Index Cooperative", @@ -8621,6 +8655,7 @@ "IONC": "IONChain", "IONOMY": "Ionomy", "IONP": "Ion Power Token", + "IONQON": "IonQ (Ondo Tokenized)", "IONX": "Charged Particles", "IONZ": "IONZ", "IOP": "Internet of People", @@ -8704,6 +8739,7 @@ "ITALOCOIN": "Italocoin", "ITAM": "ITAM Games", "ITAMCUBE": "CUBE", + "ITAON": "iShares US Aerospace and Defense ETF (Ondo Tokenized)", "ITC": "IoT Chain", "ITE": "Idle Tribe Era", "ITEM": "ITEMVERSE", @@ -9377,6 +9413,7 @@ "KONET": "KONET", "KONG": "KONG", "KONO": "Konomi Network", + "KOON": "Coca-Cola (Ondo Tokenized)", "KORA": "Kortana", "KORC": "King of Referral Coin", "KORE": "KORE Vault", @@ -9490,6 +9527,7 @@ "KWAI": "KWAI", "KWATT": "4New", "KWD": "KIWI DEFI", + "KWEBON": "KraneShares CSI China Internet ETF (Ondo Tokenized)", "KWEEN": "KWEEN", "KWENTA": "Kwenta", "KWH": "KWHCoin", @@ -9877,6 +9915,7 @@ "LMR": "Lumerin", "LMT": "LIMITUS", "LMTOKEN": "LM Token", + "LMTON": "Lockheed (Ondo Tokenized)", "LMTS": "Limitless Official Token", "LMWR": "LimeWire Token", "LMXC": "LimonX", @@ -10118,6 +10157,7 @@ "LUNES": "Lunes", "LUNG": "LunaGens", "LUNR": "Lunr Token", + "LUNRON": "Intuitive Machines (Ondo Tokenized)", "LUPIN": "LUPIN", "LUR": "Lumera", "LUS": "Luna Rush", @@ -10295,7 +10335,8 @@ "MANUSAI": "Manus AI Agent", "MANYU": "Manyu", "MANYUDOG": "MANYU", - "MAO": "Mao", + "MAO": "MAO", + "MAOMEME": "Mao", "MAOW": "MAOW", "MAP": "MAP Protocol", "MAPC": "MapCoin", @@ -11573,6 +11614,7 @@ "NBAR": "NOBAR", "NBC": "Niobium", "NBD": "Never Back Down", + "NBISON": "Nebius Group (Ondo Tokenized)", "NBIT": "NetBit", "NBL": "Nobility", "NBLU": "NuriTopia", @@ -11649,6 +11691,7 @@ "NEKOS": "Nekocoin", "NEKTAR": "Nektar Token", "NEMO": "NEMO", + "NEMON": "Newmont (Ondo Tokenized)", "NEMS": "The Nemesis", "NEO": "NEO", "NEOG": "NEO Gold", @@ -11938,6 +11981,7 @@ "NOVA": "Nova Finance", "NOVAAI": "Nova AI", "NOW": "NOW Token", + "NOWON": "ServiceNow (Ondo Tokenized)", "NOX": "NITRO", "NOXB": "Noxbox", "NPAS": "New Paradigm Assets Solution", @@ -12218,6 +12262,7 @@ "OHNOGG": "OHNHO (ohno.gg)", "OHO": "OHO", "OICOIN": "Osmium Investment Coin", + "OIHON": "VanEck Oil Services ETF (Ondo Tokenized)", "OIIAOIIA": "spinning cat", "OIK": "Space Nation", "OIL": "Oiler", @@ -12314,8 +12359,9 @@ "ONION": "DeepOnion", "ONIT": "ONBUFF", "ONIX": "Onix", - "ONL": "On.Live", + "ONL": "OneLink", "ONLINE": "Onlinebase", + "ONLIVE": "On.Live", "ONLY": "OnlyCam", "ONLYCUMIES": "OnlyCumies", "ONLYFANSCOINS": "$OFC Coin", @@ -12375,6 +12421,7 @@ "OPES": "Opes", "OPET": "ÕpetFoundation", "OPEX": "Optherium Token", + "OPG": "OpenGradient", "OPHX": "Operation Phoenix", "OPINU": "Optimus Inu", "OPIUM": "Opium", @@ -12675,6 +12722,7 @@ "PATRIOT": "Patriot", "PATTON": "Patton", "PAUL": "Elephant Penguin", + "PAVEON": "Global X US Infrastructure Development ETF (Ondo Tokenized)", "PAVIA": "Pavia", "PAVO": "Pavocoin", "PAW": "PAWSWAP", @@ -13397,6 +13445,7 @@ "PPI": "Primpy", "PPIZZA": "P Pizza", "PPL": "Pink Panther Lovers", + "PPLTON": "abrdn Physical Platinum Shares ETF (Ondo Tokenized)", "PPM": "Punk Panda Messenger", "PPN": "Puppies Network", "PPOVR": "POVR", @@ -13443,6 +13492,7 @@ "PRIMATE": "Primate", "PRIME": "Echelon Prime", "PRIMECHAIN": "PrimeChain", + "PRIMECOIN": "PrimeCoin", "PRIMEETH": "Prime Staked ETH", "PRIMEX": "Primex Finance", "PRIN": "Print The Pepe", @@ -13792,6 +13842,7 @@ "QUBE": "Qube", "QUBIC": "Qubic", "QUBITICA": "Qubitica", + "QUBTON": "Quantum Computing (Ondo Tokenized)", "QUBY": "Quby", "QUDEFI": "Qudefi", "QUE": "Queen Of Memes", @@ -13966,6 +14017,7 @@ "RDR": "Rise of Defenders", "RDS": "Reger Diamond", "RDT": "Ridotto", + "RDWON": "Redwire (Ondo Tokenized)", "RDX": "Redux Protocol", "REA": "Realisto", "REACH": "/Reach", @@ -14028,6 +14080,7 @@ "REFI": "Realfinance Network", "REFLECT": "REFLECT", "REFLECTO": "Reflecto", + "REFLECTOUSD": "Reflecto USD", "REFTOKEN": "RefToken", "REFUND": "Refund", "REG": "RealToken Ecosystem Governance", @@ -14037,6 +14090,7 @@ "REGEN": "Regen Network", "REGENT": "REGENT COIN", "REGI": "Resistance Girl", + "REGNON": "Regeneron Pharmaceuticals (Ondo Tokenized)", "REGRET": "Regret", "REHA": "Resistance Hamster", "REHAB": "NFT Rehab", @@ -14202,7 +14256,7 @@ "RIPT": "RiptideCoin", "RIPTO": "RiptoBuX", "RIS": "Riser", - "RISE": "EverRise", + "RISE": "Rise NASA", "RISECOIN": "Rise coin", "RISEP": "Rise Protocol", "RISEVISION": "Rise", @@ -14227,6 +14281,7 @@ "RKC": "Royal Kingdom Coin", "RKEY": "RKEY", "RKI": "RAKHI", + "RKLBON": "Rocket Lab (Ondo Tokenized)", "RKN": "RAKON", "RKR": "REAKTOR", "RKT": "Rock Token", @@ -14426,6 +14481,7 @@ "RUGPROOF": "Launchpad", "RUGPULL": "Captain Rug Pull", "RUGZ": "pulltherug.finance", + "RUJI": "Rujira", "RULER": "Ruler Protocol", "RUM": "RUM Pirates of The Arrland Token", "RUN": "Speedrun", @@ -14440,7 +14496,7 @@ "RUP": "Rupee", "RUPX": "Rupaya", "RURI": "Ruri - Truth Terminal's Crush", - "RUSD": "Reflecto USD", + "RUSD": "Royal Dollar", "RUSH": "RUSH COIN", "RUSHCMC": "RUSHCMC", "RUSSELL": "Russell", @@ -14659,6 +14715,7 @@ "SBSC": "Subscriptio", "SBT": "SOLBIT", "SBTC": "Super Bitcoin", + "SBUXON": "Starbucks (Ondo Tokenized)", "SC": "Siacoin", "SC20": "Shine Chain", "SCA": "Scallop", @@ -14675,6 +14732,7 @@ "SCASH": "SpaceCash", "SCAT": "Sad Cat Token", "SCC": "StockChain Coin", + "SCCOON": "Southern Copper (Ondo Tokenized)", "SCCP": "S.C. Corinthians Fan Token", "SCDS": "Shrine Cloud Storage Network", "SCF": "Smoking Chicken Fish", @@ -15054,6 +15112,7 @@ "SHX": "Stronghold Token", "SHXV1": "Stronghold Token v1", "SHY": "Shytoshi Kusama", + "SHYON": "iShares 1-3 Year Treasury Bond ETF (Ondo Tokenized)", "SHYTCOIN": "ShytCoin", "SI": "Siren", "SI14": "Si14", @@ -15248,6 +15307,7 @@ "SLUGDENG": "SLUG DENG", "SLUMBO": "SLUMBO", "SLVLUSD": "Staked Level USD", + "SLVN": "SLVNToken", "SLVON": "iShares Silver Trust (Ondo Tokenized)", "SLVX": "eToro Silver", "SLX": "SLIMEX", @@ -15339,6 +15399,7 @@ "SNC": "SunContract", "SNCT": "SnakeCity", "SND": "Sandcoin", + "SNDKON": "SanDisk (Ondo Tokenized)", "SNE": "StrongNode", "SNEED": "Sneed", "SNEK": "Snek", @@ -15632,10 +15693,11 @@ "SPIDERMAN": "SPIDERMAN BITCOIN", "SPIDEY": "Spidey", "SPIK": "Spike", - "SPIKE": "Spiking", + "SPIKE": "SPIKE", "SPIKE1984": "Spike 1984", "SPIKECOIN": "SPIKE", "SPIKEFURIE": "SPIKE", + "SPIKING": "Spiking", "SPILLWAYS": "SpillWays", "SPIN": "SPIN Protocol", "SPINT": "Spintria", @@ -15934,6 +15996,7 @@ "STRA": "STRAY", "STRAKS": "Straks", "STRAT": "Strategic Hub for Innovation in Blockchain", + "STRAWBE": "Strawberry In Bloom", "STRAX": "Stratis", "STRAY": "Stray Dog", "STRAYDOG": "Stray Dog", @@ -15981,6 +16044,7 @@ "STV": "Sativa Coin", "STWEMIX": "Staked WEMIX", "STX": "Stacks", + "STXON": "Seagate (Ondo Tokenized)", "STYL": "Stylike Governance", "STYLE": "Style", "STZ": "99Starz", @@ -16801,6 +16865,7 @@ "TRADECHAIN": "Trade Chain", "TRADETIDE": "Trade Tide Token", "TRADEX": "TradeX AI", + "TRADIX": "Tradix", "TRADOOR": "Tradoor", "TRAI": "Trackgood AI", "TRAID": "Traid", @@ -16881,6 +16946,7 @@ "TROLLRUN": "TROLL", "TROLLS": "trolls in a memes world", "TRONBETLIVE": "TRONbetLive", + "TRONBULL": "Tron Bull", "TRONDOG": "TronDog", "TRONI": "Tron Inu", "TRONP": "Donald Tronp", @@ -17015,6 +17081,7 @@ "TTM": "Tradetomato", "TTN": "TTN", "TTNT": "TITA Project", + "TTPA": "TRUMPTOPIA", "TTT": "TRUMPETTOKEN", "TTTU": "T-Project", "TTU": "TaTaTu", @@ -17077,7 +17144,7 @@ "TWP": "TrumpWifPanda", "TWT": "Trust Wallet Token", "TWURTLE": "twurtle the turtle", - "TX": "Tradix", + "TX": "tx", "TX20": "Trex20", "TXA": "TXA", "TXAG": "tSILVER", @@ -17160,6 +17227,7 @@ "UDT": "Unlock Protocol", "UE": "UE Coin", "UEC": "United Emirates Coin", + "UECON": "Uranium Energy (Ondo Tokenized)", "UEDC": "United Emirate Decentralized Coin", "UENC": "UniversalEnergyChain", "UET": "Useless Ethereum Token", @@ -17239,6 +17307,7 @@ "UNF": "Unfed Coin", "UNFI": "Unifi Protocol DAO", "UNFK": "UNFK", + "UNGON": "US Natural Gas Fund (Ondo Tokenized)", "UNHX": "UnitedHealth xStock", "UNI": "Uniswap Protocol Token", "UNIART": "UNIART", @@ -17276,6 +17345,7 @@ "UNITE": "Unite", "UNITED": "UnitedCoins", "UNITEDTRADERS": "United Traders Token", + "UNITOKEN": "Uni Token", "UNITPROV2": "Unit Protocol New", "UNITRADE": "UniTrade", "UNITREEAI": "Unitree G1 AI", @@ -17292,17 +17362,18 @@ "UNO": "UnoRe", "UNOB": "Unobtanium", "UNP": "UNIPOLY", + "UNPON": "Union Pacific Corporation (Ondo Tokenized)", "UNQ": "UNQ", "UNQT": "Unique Utility Token", "UNR": "Unirealchain", "UNRC": "UniversalRoyalCoin", "UNS": "UNS TOKEN", "UNSHETH": "unshETH Ether", - "UNT": "Uni Token", + "UNT": "UnityWallet Token", "UNW": "UniWorld", "UOP": "Utopia Genesis Foundation", "UOS": "UOS", - "UP": "UpToken", + "UP": "Superform", "UPC": "UPCX", "UPCG": "Upcomings", "UPCO2": "Universal Carbon", @@ -17317,6 +17388,7 @@ "UPRO": "ULTRAPRO", "UPS": "UPFI Network", "UPT": "UPROCK", + "UPTOKEN": "UpToken", "UPTOP": "UPTOP", "UPTOS": "UPTOS", "UPUNK": "Unicly CryptoPunks Collection", @@ -17327,6 +17399,7 @@ "URAC": "Uranus", "URALS": "Urals Coin", "URANUS": "Uranus", + "URAON": "Global X Uranium ETF (Ondo Tokenized)", "URFA": "Urfaspor Token", "URMOM": "urmom", "URO": "Urolithin A", @@ -17575,7 +17648,7 @@ "VEC2": "VectorCoin 2.0", "VECT": "Vectorium", "VECTOR": "VectorChat.ai", - "VEE": "BLOCKv", + "VEE": "Vee Token", "VEED": "VEED", "VEEN": "LIVEEN", "VEETOKEN": "Vee Token", @@ -17638,6 +17711,7 @@ "VEXT": "Veloce", "VFIL": "Venus Filecoin", "VFOX": "VFOX", + "VFSON": "VinFast Auto (Ondo Tokenized)", "VFT": "Value Finance", "VFX": "ViFoxCoin", "VFY": "zkVerify", @@ -17758,6 +17832,7 @@ "VNM": "Venom", "VNN": "VINU Network", "VNO": "Veno Finance", + "VNQON": "Vanguard Real Estate ETF (Ondo Tokenized)", "VNST": "VNST Stablecoin", "VNT": "VNT Chain", "VNTR": "Venture Mind AI", @@ -17831,6 +17906,7 @@ "VRSW": "VirtuSwap", "VRT": "Venus Reward Token", "VRTX": "Vertex Protocol", + "VRTXON": "Vertex Pharmaceuticals (Ondo Tokenized)", "VRTY": "Verity", "VRX": "Verox", "VS": "veSync", @@ -17844,6 +17920,7 @@ "VSOL": "VSolidus", "VSP": "Vesper Finance", "VSTA": "Vesta Finance", + "VSTON": "Vistra (Ondo Tokenized)", "VSTR": "Vestra DAO", "VSUI": "Volo Staked SUI", "VSX": "Versus-X", @@ -18038,6 +18115,7 @@ "WCUSD": "Wrapped Celo Dollar", "WDAI": "Dai (Wormhole)", "WDC": "WorldCoin", + "WDCON": "Western Digital (Ondo Tokenized)", "WDOG": "Winterdog", "WDOGE": "Wrapped Dogecoin", "WDOT": "WDOT", @@ -18273,6 +18351,7 @@ "WMM": "Weird Medieval Memes", "WMN": "WebMind Network", "WMNT": "Wrapped Mantle", + "WMON": "Waste Management (Ondo Tokenized)", "WMOXY": "Moxy", "WMT": "World Mobile Token v1", "WMTON": "Walmart (Ondo Tokenized)", @@ -18304,9 +18383,10 @@ "WOID": "WORLD ID", "WOJ": "Wojak Finance", "WOJA": "Wojak", - "WOJAK": "Wojak", + "WOJAK": "wojak", "WOJAK2": "Wojak 2.0 Coin", "WOJAKC": "Wojak Coin", + "WOJAKIO": "Wojak", "WOKB": "Wrapped OKB", "WOKIE": "Wokie Plumpkin by Virtuals", "WOKT": "Wrapped OKT", @@ -18462,6 +18542,7 @@ "WXRP": "Wrapped XRP", "WXT": "WXT", "WYAC": "Woman Yelling At Cat", + "WYDE": "WYDE: End Hunger", "WYN": "Wynn", "WYNN": "Anita Max Wynn", "WYS": "Wysker", @@ -18683,6 +18764,7 @@ "XMN": "xMoney", "XMO": "Monero Original", "XMON": "XMON", + "XMONEY": "X Money", "XMOON": "r/CryptoCurrency Moons v1", "XMP": "Mapt.Coin", "XMR": "Monero", @@ -18741,7 +18823,7 @@ "XPL": "Plasma", "XPLA": "XPLA", "XPLL": "ParallelChain", - "XPM": "PrimeCoin", + "XPM": "XPMarket Token", "XPN": "PANTHEON X", "XPND": "Time Raiders", "XPNET": "XP Network", @@ -18860,6 +18942,7 @@ "XVS": "Venus", "XWC": "WhiteCoin", "XWG": "X World Games", + "XWGT": "Wodo Gaming Token", "XWIN": "xWIN Finance", "XWP": "Swap", "XWT": "World Trade Funds", @@ -19102,7 +19185,8 @@ "ZEBU": "ZEBU", "ZEC": "ZCash", "ZECD": "ZCashDarkCoin", - "ZED": "ZedCoins", + "ZED": "ZED Token", + "ZEDCOIN": "ZedCoin", "ZEDD": "ZedDex", "ZEDTOKEN": "Zed Token", "ZEDX": "ZEDX Сoin", diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 5603964c5..66cda1d78 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,6 +1,5 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; -import { groupBy } from '@ghostfolio/common/helper'; import { PortfolioPosition, PortfolioReportRule, @@ -9,6 +8,7 @@ import { } from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; +import { groupBy } from 'lodash'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; @@ -41,10 +41,10 @@ export abstract class Rule implements RuleInterface { public groupCurrentHoldingsByAttribute( holdings: PortfolioPosition[], - attribute: keyof PortfolioPosition, + attribute: `assetProfile.${Extract}`, baseCurrency: string ) { - return Array.from(groupBy(attribute, holdings).entries()).map( + return Object.entries(groupBy(holdings, attribute)).map( ([attributeValue, objs]) => ({ groupKey: attributeValue, investment: objs.reduce( @@ -59,7 +59,7 @@ export abstract class Rule implements RuleInterface { new Big(currentValue.quantity) .mul(currentValue.marketPrice ?? 0) .toNumber(), - currentValue.currency, + currentValue.assetProfile.currency, baseCurrency ), 0 diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts index 372b0bb06..12303fd92 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts @@ -27,9 +27,10 @@ export class AssetClassClusterRiskEquity extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute( this.holdings, - 'assetClass', + 'assetProfile.assetClass', ruleSettings.baseCurrency ); + let totalValue = 0; const equityValueInBaseCurrency = diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts index 404b4cd32..fd7c00f11 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts @@ -27,9 +27,10 @@ export class AssetClassClusterRiskFixedIncome extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute( this.holdings, - 'assetClass', + 'assetProfile.assetClass', ruleSettings.baseCurrency ); + let totalValue = 0; const fixedIncomeValueInBaseCurrency = diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index 83c72eeb8..6890fecd6 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -27,7 +27,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( this.holdings, - 'currency', + 'assetProfile.currency', ruleSettings.baseCurrency ); diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index ad8e84a99..b19508d3e 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -44,7 +44,6 @@ export class ConfigurationService { ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }), ENABLE_FEATURE_AUTH_OIDC: bool({ default: false }), ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }), - ENABLE_FEATURE_BULL_BOARD: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 72136dc04..30ad81c09 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -218,16 +218,18 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { }; }) ?? []; - response.sectors = ( - assetProfile.topHoldings?.sectorWeightings ?? [] - ).flatMap((sectorWeighting) => { - return Object.entries(sectorWeighting).map(([sector, weight]) => { - return { - name: this.parseSector(sector), - weight: weight as number - }; + response.sectors = (assetProfile.topHoldings?.sectorWeightings ?? []) + .flatMap((sectorWeighting) => { + return Object.entries(sectorWeighting).map(([sector, weight]) => { + return { + name: this.parseSector(sector), + weight: weight as number + }; + }); + }) + .filter(({ weight }) => { + return weight > 0; }); - }); } else if ( assetSubClass === 'STOCK' && assetProfile.summaryProfile?.country 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 e7c7e8a2e..5f0a6928a 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -82,6 +82,7 @@ export class DataProviderService implements OnModuleInit { return false; } + // TODO: Change symbol in response to assetProfileIdentifier public async getAssetProfiles(items: AssetProfileIdentifier[]): Promise<{ [symbol: string]: Partial; }> { @@ -330,6 +331,7 @@ export class DataProviderService implements OnModuleInit { }); } + // TODO: Change symbol in response to assetProfileIdentifier public async getHistorical( aItems: AssetProfileIdentifier[], aGranularity: Granularity = 'month', @@ -395,6 +397,7 @@ export class DataProviderService implements OnModuleInit { } } + // TODO: Change symbol in response to assetProfileIdentifier public async getHistoricalRaw({ assetProfileIdentifiers, from, @@ -508,6 +511,7 @@ export class DataProviderService implements OnModuleInit { return result; } + // TODO: Change symbol in response to assetProfileIdentifier public async getQuotes({ items, requestTimeout, diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 27391130e..d9a43fc50 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -1,5 +1,6 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; +import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { DataProviderInterface, GetAssetProfileParams, @@ -122,7 +123,9 @@ export class FinancialModelingPrepService ).then((res) => res.json()); if (!assetProfile) { - throw new Error(`${symbol} not found`); + throw new AssetProfileDelistedError( + `No data found, ${symbol} (${this.getName()}) may be delisted` + ); } const { assetClass, assetSubClass } = diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 9664ae144..eb3ac86a3 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -20,7 +20,6 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_AUTH_GOOGLE: boolean; ENABLE_FEATURE_AUTH_OIDC: boolean; ENABLE_FEATURE_AUTH_TOKEN: boolean; - ENABLE_FEATURE_BULL_BOARD: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts index d163f0d29..5672df5e8 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts @@ -19,18 +19,14 @@ import { DataGatheringProcessor } from './data-gathering.processor'; @Module({ imports: [ - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forFeature({ - adapter: BullAdapter, - name: DATA_GATHERING_QUEUE, - options: { - displayName: 'Data Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' - } - }) - ] - : []), + BullBoardModule.forFeature({ + adapter: BullAdapter, + name: DATA_GATHERING_QUEUE, + options: { + displayName: 'Data Gathering', + readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + } + }), BullModule.registerQueue({ limiter: { duration: ms('4 seconds'), diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index 1260f1cf0..c90f826f6 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -25,18 +25,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; imports: [ AccountBalanceModule, ActivitiesModule, - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forFeature({ - adapter: BullAdapter, - name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, - options: { - displayName: 'Portfolio Snapshot Computation', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' - } - }) - ] - : []), + BullBoardModule.forFeature({ + adapter: BullAdapter, + name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, + options: { + displayName: 'Portfolio Snapshot Computation', + readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + } + }), BullModule.registerQueue({ name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, settings: { diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts index 1818dd4ec..60b963c69 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts @@ -13,7 +13,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service'; @Module({ exports: [BullModule, StatisticsGatheringService], imports: [ - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' + ...(process.env.ENABLE_FEATURE_STATISTICS === 'true' ? [ BullBoardModule.forFeature({ adapter: BullAdapter, diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts index 07fc32585..1312d49ea 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts @@ -93,12 +93,25 @@ export class StatisticsGatheringProcessor { @Process(GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME) public async gatherUptimeStatistics() { + const monitorId = await this.propertyService.getByKey( + PROPERTY_BETTER_UPTIME_MONITOR_ID + ); + + if (!monitorId) { + Logger.log( + `Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`, + 'StatisticsGatheringProcessor' + ); + + return; + } + Logger.log( 'Uptime statistics gathering has been started', 'StatisticsGatheringProcessor' ); - const uptime = await this.getUptime(); + const uptime = await this.getUptime(monitorId); await this.propertyService.put({ key: PROPERTY_UPTIME, @@ -179,12 +192,8 @@ export class StatisticsGatheringProcessor { } } - private async getUptime(): Promise { + private async getUptime(monitorId: string): Promise { try { - const monitorId = await this.propertyService.getByKey( - PROPERTY_BETTER_UPTIME_MONITOR_ID - ); - const { data } = await fetch( `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( subDays(new Date(), 90), diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index f51315e91..d9b279040 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -1,6 +1,10 @@ import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; +import { + DEFAULT_DATE_RANGE, + DEFAULT_PAGE_SIZE, + NUMERICAL_PRECISION_THRESHOLD_6_FIGURES +} from '@ghostfolio/common/config'; import { CreateAccountBalanceDto } from '@ghostfolio/common/dtos'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { @@ -20,7 +24,6 @@ import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table'; import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -34,6 +37,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; +import { PageEvent } from '@angular/material/paginator'; import { Sort, SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -57,7 +61,6 @@ import { AccountDetailDialogParams } from './interfaces/interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-flex flex-column h-100' }, imports: [ - CommonModule, GfAccountBalancesComponent, GfActivitiesTableComponent, GfDialogFooterComponent, @@ -95,6 +98,8 @@ export class GfAccountDetailDialogComponent implements OnInit { protected isLoadingActivities: boolean; protected isLoadingChart: boolean; protected name: string | null; + protected pageIndex = 0; + protected pageSize = DEFAULT_PAGE_SIZE; protected platformName: string; protected sortColumn = 'date'; protected sortDirection: SortDirection = 'desc'; @@ -135,6 +140,21 @@ export class GfAccountDetailDialogComponent implements OnInit { this.initialize(); } + protected onAddAccountBalance(accountBalance: CreateAccountBalanceDto) { + this.dataService + .postAccountBalance(accountBalance) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.initialize(); + }); + } + + protected onChangePage(page: PageEvent) { + this.pageIndex = page.pageIndex; + + this.fetchActivities(); + } + protected onCloneActivity(aActivity: Activity) { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink, @@ -150,15 +170,6 @@ export class GfAccountDetailDialogComponent implements OnInit { this.dialogRef.close(); } - protected onAddAccountBalance(accountBalance: CreateAccountBalanceDto) { - this.dataService - .postAccountBalance(accountBalance) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.initialize(); - }); - } - protected onDeleteAccountBalance(aId: string) { this.dataService .deleteAccountBalance(aId) @@ -289,8 +300,10 @@ export class GfAccountDetailDialogComponent implements OnInit { this.dataService .fetchActivities({ filters: [{ id: this.data.accountId, type: 'ACCOUNT' }], + skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn, - sortDirection: this.sortDirection + sortDirection: this.sortDirection, + take: this.pageSize }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ activities, count }) => { @@ -318,7 +331,7 @@ export class GfAccountDetailDialogComponent implements OnInit { type: 'ACCOUNT' } ], - range: 'max', + range: DEFAULT_DATE_RANGE, withExcludedAccounts: true, withItems: true }) diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index e63fb9fc1..cd397e35e 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -91,8 +91,8 @@ @@ -120,6 +120,8 @@ [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" [locale]="user?.settings?.locale" + [pageIndex]="pageIndex" + [pageSize]="pageSize" [showAccountColumn]="false" [showActions]=" !data.hasImpersonationId && @@ -133,6 +135,7 @@ (activityToClone)="onCloneActivity($event)" (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" + (pageChanged)="onChangePage($event)" (sortChanged)="onSortChanged($event)" /> diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html index 1570ce82b..d57704b86 100644 --- a/apps/client/src/app/components/admin-jobs/admin-jobs.html +++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html @@ -172,7 +172,7 @@ } @else if (element.state === 'failed') { { return assetSubClass !== 'CASH'; @@ -146,37 +146,39 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { type: 'PRESET_ID' as Filter['type'] } ]; - public benchmarks: Partial[]; - public currentDataSource: DataSource; - public currentSymbol: string; - public dataSource = new MatTableDataSource(); - public defaultDateFormat: string; - public deviceType: string; - public displayedColumns: string[] = []; - public filters$ = new Subject(); - public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; - public hasPermissionForSubscription: boolean; - public info: InfoItem; - public isLoading = true; - public isUUID = isUUID; - public placeholder = ''; - public pageSize = DEFAULT_PAGE_SIZE; - public selection: SelectionModel>; - public totalItems = 0; - public user: User; - - public constructor( - public adminMarketDataService: AdminMarketDataService, - private adminService: AdminService, - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private dialog: MatDialog, - private route: ActivatedRoute, - private router: Router, - private userService: UserService - ) { + protected dataSource = new MatTableDataSource(); + protected defaultDateFormat: string; + protected readonly displayedColumns: string[] = []; + protected readonly filters$ = new Subject(); + protected isLoading = true; + protected readonly isUUID = isUUID; + protected pageSize = DEFAULT_PAGE_SIZE; + protected placeholder = ''; + protected readonly selection = new SelectionModel(true); + protected totalItems = 0; + protected user: User; + + private activeFilters: Filter[] = []; + private benchmarks: Partial[]; + private readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + private readonly hasPermissionForSubscription: boolean; + private readonly info: InfoItem; + private readonly paginator = viewChild.required(MatPaginator); + private readonly sort = viewChild.required(MatSort); + + private readonly adminService = inject(AdminService); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public constructor() { this.info = this.dataService.fetchInfo(); this.hasPermissionForSubscription = hasPermission( @@ -255,14 +257,14 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { } public ngAfterViewInit() { - this.sort.sortChange.subscribe( + this.sort().sortChange.subscribe( ({ active: sortColumn, direction }: Sort) => { - this.paginator.pageIndex = 0; + this.paginator().pageIndex = 0; this.loadData({ sortColumn, sortDirection: direction, - pageIndex: this.paginator.pageIndex + pageIndex: this.paginator().pageIndex }); } ); @@ -272,24 +274,24 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { const { benchmarks } = this.dataService.fetchInfo(); this.benchmarks = benchmarks; - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - - this.selection = new SelectionModel(true); } - public onChangePage(page: PageEvent) { + protected onChangePage(page: PageEvent) { this.loadData({ pageIndex: page.pageIndex, - sortColumn: this.sort.active, - sortDirection: this.sort.direction + sortColumn: this.sort().active, + sortDirection: this.sort().direction }); } - public onDeleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) { + protected onDeleteAssetProfile({ + dataSource, + symbol + }: AssetProfileIdentifier) { this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); } - public onDeleteAssetProfiles() { + protected onDeleteAssetProfiles() { this.adminMarketDataService.deleteAssetProfiles( this.selection.selected.map(({ dataSource, symbol }) => { return { dataSource, symbol }; @@ -297,7 +299,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { ); } - public onGather7Days() { + protected onGather7Days() { this.adminService .gather7Days() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -308,7 +310,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); } - public onGatherMax() { + protected onGatherMax() { this.adminService .gatherMax() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -319,31 +321,14 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); } - public onGatherProfileData() { + protected onGatherProfileData() { this.adminService .gatherProfileData() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); } - public onGatherProfileDataBySymbol({ - dataSource, - symbol - }: AssetProfileIdentifier) { - this.adminService - .gatherProfileDataBySymbol({ dataSource, symbol }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - } - - public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) { - this.adminService - .gatherSymbol({ dataSource, symbol }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - } - - public onOpenAssetProfileDialog({ + protected onOpenAssetProfileDialog({ dataSource, symbol }: AssetProfileIdentifier) { @@ -375,8 +360,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { ? Number.MAX_SAFE_INTEGER : DEFAULT_PAGE_SIZE; - if (pageIndex === 0 && this.paginator) { - this.paginator.pageIndex = 0; + if (pageIndex === 0 && this.paginator()) { + this.paginator().pageIndex = 0; } this.placeholder = @@ -406,7 +391,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }; }) ); - this.dataSource.sort = this.sort; + this.dataSource.sort = this.sort(); this.isLoading = false; @@ -435,12 +420,13 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { data: { dataSource, symbol, - colorScheme: this.user?.settings.colorScheme, - deviceType: this.deviceType, - locale: this.user?.settings?.locale - }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + colorScheme: + this.user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME, + deviceType: this.deviceType(), + locale: this.user?.settings?.locale ?? locale + } satisfies AssetProfileDialogParams, + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -471,10 +457,10 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { >(GfCreateAssetProfileDialogComponent, { autoFocus: false, data: { - deviceType: this.deviceType, - locale: this.user?.settings?.locale - }, - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + deviceType: this.deviceType(), + locale: this.user?.settings?.locale ?? locale + } satisfies CreateAssetProfileDialogParams, + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 23615969b..e2c6d1a87 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -309,11 +309,11 @@

- {{ assetProfile?.name ?? data.symbol }} + {{ assetProfile?.name ?? data.symbol }}

@@ -18,12 +18,12 @@ Compare with... - @for (symbolProfile of benchmarks; track symbolProfile) { + @for (symbolProfile of benchmarks(); track symbolProfile) { {{ symbolProfile.name }} @@ -41,7 +41,7 @@
- @if (isLoading) { + @if (isLoading()) {
diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index 2ecefc311..d2dc9e1bb 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -22,12 +22,11 @@ import { ChangeDetectionStrategy, Component, type ElementRef, - EventEmitter, - Input, + input, OnChanges, OnDestroy, - Output, - ViewChild + output, + viewChild } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatSelectModule } from '@angular/material/select'; @@ -68,24 +67,25 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; templateUrl: './benchmark-comparator.component.html' }) export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { - @Input() benchmark: Partial; - @Input() benchmarkDataItems: LineChartItem[] = []; - @Input() benchmarks: Partial[]; - @Input() colorScheme: ColorScheme; - @Input() isLoading: boolean; - @Input() locale = getLocale(); - @Input() performanceDataItems: LineChartItem[]; - @Input() user: User; + public readonly benchmark = input>(); + public readonly benchmarkDataItems = input([]); + public readonly benchmarks = input[]>(); + public readonly colorScheme = input.required(); + public readonly isLoading = input(); + public readonly locale = input(getLocale()); + public readonly performanceDataItems = input.required(); + public readonly user = input(); - @Output() benchmarkChanged = new EventEmitter(); + public readonly benchmarkChanged = output(); - @ViewChild('chartCanvas') chartCanvas: ElementRef; - - public chart: Chart<'line'>; - public hasPermissionToAccessAdminControl: boolean; - public routerLinkAdminControlMarketData = + protected chart: Chart<'line'>; + protected hasPermissionToAccessAdminControl: boolean; + protected readonly routerLinkAdminControlMarketData = internalRoutes.adminControl.subRoutes.marketData.routerLink; + private readonly chartCanvas = + viewChild.required>('chartCanvas'); + public constructor() { Chart.register( annotationPlugin, @@ -104,27 +104,27 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { public ngOnChanges() { this.hasPermissionToAccessAdminControl = hasPermission( - this.user?.permissions, + this.user()?.permissions, permissions.accessAdminControl ); - if (this.performanceDataItems) { + if (this.performanceDataItems()) { this.initialize(); } } - public onChangeBenchmark(symbolProfileId: string) { - this.benchmarkChanged.next(symbolProfileId); - } - public ngOnDestroy() { this.chart?.destroy(); } + protected onChangeBenchmark(symbolProfileId: string) { + this.benchmarkChanged.emit(symbolProfileId); + } + private initialize() { const benchmarkDataValues: Record = {}; - for (const { date, value } of this.benchmarkDataItems) { + for (const { date, value } of this.benchmarkDataItems()) { benchmarkDataValues[date] = value; } @@ -134,8 +134,11 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderWidth: 2, - data: this.performanceDataItems.map(({ date, value }) => { - return { x: parseDate(date).getTime(), y: value * 100 }; + data: this.performanceDataItems().map(({ date, value }) => { + return { + x: parseDate(date)?.getTime() ?? null, + y: value * 100 + }; }), label: $localize`Portfolio` }, @@ -143,9 +146,9 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, borderWidth: 2, - data: this.performanceDataItems.map(({ date }) => { + data: this.performanceDataItems().map(({ date }) => { return { - x: parseDate(date).getTime(), + x: parseDate(date)?.getTime() ?? null, y: benchmarkDataValues[date] }; }), @@ -163,7 +166,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { this.chart.update(); } else { - this.chart = new Chart(this.chartCanvas.nativeElement, { + this.chart = new Chart<'line'>(this.chartCanvas().nativeElement, { data, options: { animation: false, @@ -172,7 +175,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { tension: 0 }, point: { - hoverBackgroundColor: getBackgroundColor(this.colorScheme), + hoverBackgroundColor: getBackgroundColor(this.colorScheme()), hoverRadius: 2, radius: 0 } @@ -183,7 +186,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { annotation: { annotations: { yAxis: { - borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, + borderColor: `rgba(${getTextColor(this.colorScheme())}, 0.1)`, borderWidth: 1, scaleID: 'y', type: 'line', @@ -196,14 +199,14 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { }, tooltip: this.getTooltipPluginConfiguration(), verticalHoverLine: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` + color: `rgba(${getTextColor(this.colorScheme())}, 0.1)` } }, responsive: true, scales: { x: { border: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, + color: `rgba(${getTextColor(this.colorScheme())}, 0.1)`, width: 1 }, display: true, @@ -212,7 +215,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { }, type: 'time', time: { - tooltipFormat: getDateFormatString(this.locale), + tooltipFormat: getDateFormatString(this.locale()), unit: 'year' } }, @@ -228,7 +231,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { tick.value === scale.max || tick.value === scale.min ) { - return `rgba(${getTextColor(this.colorScheme)}, 0.1)`; + return `rgba(${getTextColor(this.colorScheme())}, 0.1)`; } return 'transparent'; @@ -247,7 +250,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { } }, plugins: [ - getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme) + getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme()) ], type: 'line' }); @@ -258,8 +261,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { private getTooltipPluginConfiguration(): Partial> { return { ...getTooltipOptions({ - colorScheme: this.colorScheme, - locale: this.locale, + colorScheme: this.colorScheme(), + locale: this.locale(), unit: '%' }), mode: 'index', diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index ab5820e2d..ae3121861 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -1,14 +1,14 @@ - @if (user) { -
+ @if (user()) { + @@ -18,13 +18,13 @@ class="d-none d-sm-block rounded" i18n mat-button - [ngClass]="{ + [class]="{ 'font-weight-bold': - currentRoute === internalRoutes.home.path || - currentRoute === internalRoutes.zen.path, + currentRoute() === internalRoutes.home.path || + currentRoute() === internalRoutes.zen.path, 'text-decoration-underline': - currentRoute === internalRoutes.home.path || - currentRoute === internalRoutes.zen.path + currentRoute() === internalRoutes.home.path || + currentRoute() === internalRoutes.zen.path }" [routerLink]="['/']" >OverviewPortfolioAccountsAdmin ControlResources @if ( - hasPermissionForSubscription && user?.subscription?.type === 'Basic' + hasPermissionForSubscription && user()?.subscription?.type === 'Basic' ) {
  • Pricing - @if (currentRoute !== routePricing && hasPromotion) { + @if (currentRoute() !== routePricing && hasPromotion()) { % } @@ -115,9 +116,9 @@ class="d-none d-sm-block rounded" i18n mat-button - [ngClass]="{ - 'font-weight-bold': currentRoute === routeAbout, - 'text-decoration-underline': currentRoute === routeAbout + [class]="{ + 'font-weight-bold': currentRoute() === routeAbout, + 'text-decoration-underline': currentRoute() === routeAbout }" [routerLink]="routerLinkAbout" >About @if ( - hasPermissionForSubscription && user?.subscription?.type === 'Basic' + hasPermissionForSubscription && + user()?.subscription?.type === 'Basic' ) { - @if (user.subscription.offer.isRenewal) { + @if (user().subscription.offer.isRenewal) { Renew Plan } @else { Upgrade Plan @@ -199,7 +203,7 @@ >
    } - @if (user?.access?.length > 0) { + @if (user()?.access?.length > 0) { - @for (accessItem of user?.access; track accessItem) { + @for (accessItem of user()?.access; track accessItem) {
  • } - @if (user === null) { -
    + @if (user() === null) { + @@ -352,9 +353,9 @@ class="d-none d-sm-block rounded" i18n mat-button - [ngClass]="{ - 'font-weight-bold': currentRoute === routeFeatures, - 'text-decoration-underline': currentRoute === routeFeatures + [class]="{ + 'font-weight-bold': currentRoute() === routeFeatures, + 'text-decoration-underline': currentRoute() === routeFeatures }" [routerLink]="routerLinkFeatures" >FeaturesAbout Pricing - @if (currentRoute !== routePricing && hasPromotion) { + @if (currentRoute() !== routePricing && hasPromotion()) { % } @@ -399,9 +400,9 @@ class="d-none d-sm-block rounded" i18n mat-button - [ngClass]="{ - 'font-weight-bold': currentRoute === routeMarkets, - 'text-decoration-underline': currentRoute === routeMarkets + [class]="{ + 'font-weight-bold': currentRoute() === routeMarkets, + 'text-decoration-underline': currentRoute() === routeMarkets }" [routerLink]="routerLinkMarkets" >MarketsSign in - @if (currentRoute !== 'register' && hasPermissionToCreateUser) { + @if (currentRoute() !== 'register' && hasPermissionToCreateUser) {
  • (); - - @ViewChild('assistant') assistantElement: GfAssistantComponent; - @ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger; - - public hasFilters: boolean; - public hasImpersonationId: boolean; - public hasPermissionForAuthGoogle: boolean; - public hasPermissionForAuthOidc: boolean; - public hasPermissionForAuthToken: boolean; - public hasPermissionForSubscription: boolean; - public hasPermissionToAccessAdminControl: boolean; - public hasPermissionToAccessAssistant: boolean; - public hasPermissionToAccessFearAndGreedIndex: boolean; - public hasPermissionToCreateUser: boolean; - public impersonationId: string; - public internalRoutes = internalRoutes; - public isMenuOpen: boolean; - public routeAbout = publicRoutes.about.path; - public routeFeatures = publicRoutes.features.path; - public routeMarkets = publicRoutes.markets.path; - public routePricing = publicRoutes.pricing.path; - public routeResources = publicRoutes.resources.path; - public routerLinkAbout = publicRoutes.about.routerLink; - public routerLinkAccount = internalRoutes.account.routerLink; - public routerLinkAccounts = internalRoutes.accounts.routerLink; - public routerLinkAdminControl = internalRoutes.adminControl.routerLink; - public routerLinkFeatures = publicRoutes.features.routerLink; - public routerLinkMarkets = publicRoutes.markets.routerLink; - public routerLinkPortfolio = internalRoutes.portfolio.routerLink; - public routerLinkPricing = publicRoutes.pricing.routerLink; - public routerLinkRegister = publicRoutes.register.routerLink; - public routerLinkResources = publicRoutes.resources.routerLink; - - public constructor( - private dataService: DataService, - private destroyRef: DestroyRef, - private dialog: MatDialog, - private impersonationStorageService: ImpersonationStorageService, - private layoutService: LayoutService, - private notificationService: NotificationService, - private router: Router, - private settingsStorageService: SettingsStorageService, - private tokenStorageService: TokenStorageService, - private userService: UserService - ) { + public readonly currentRoute = input.required(); + public readonly deviceType = input.required(); + public readonly hasPermissionToChangeDateRange = input.required(); + public readonly hasPermissionToChangeFilters = input.required(); + public readonly hasPromotion = input.required(); + public readonly hasTabs = input.required(); + public readonly info = input.required(); + public readonly pageTitle = input.required(); + public readonly user = input.required(); + + public readonly signOut = output(); + + protected readonly assistantElement = + viewChild.required('assistant'); + protected readonly assistentMenuTriggerElement = + viewChild.required('assistantTrigger'); + + protected hasFilters: boolean; + protected hasImpersonationId: boolean; + protected hasPermissionForAuthGoogle: boolean; + protected hasPermissionForAuthOidc: boolean; + protected hasPermissionForAuthToken: boolean; + protected hasPermissionForSubscription: boolean; + protected hasPermissionToAccessAdminControl: boolean; + protected hasPermissionToAccessAssistant: boolean; + protected hasPermissionToAccessFearAndGreedIndex: boolean; + protected hasPermissionToCreateUser: boolean; + protected impersonationId: string; + protected readonly internalRoutes = internalRoutes; + protected isMenuOpen: boolean; + protected readonly routeAbout = publicRoutes.about.path; + protected readonly routeFeatures = publicRoutes.features.path; + protected readonly routeMarkets = publicRoutes.markets.path; + protected readonly routePricing = publicRoutes.pricing.path; + protected readonly routeResources = publicRoutes.resources.path; + protected readonly routerLinkAbout = publicRoutes.about.routerLink; + protected readonly routerLinkAccount = internalRoutes.account.routerLink; + protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; + protected readonly routerLinkAdminControl = + internalRoutes.adminControl.routerLink; + protected readonly routerLinkFeatures = publicRoutes.features.routerLink; + protected readonly routerLinkMarkets = publicRoutes.markets.routerLink; + protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink; + protected readonly routerLinkPricing = publicRoutes.pricing.routerLink; + protected readonly routerLinkRegister = publicRoutes.register.routerLink; + protected readonly routerLinkResources = publicRoutes.resources.routerLink; + + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly dialog = inject(MatDialog); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly layoutService = inject(LayoutService); + private readonly notificationService = inject(NotificationService); + private readonly router = inject(Router); + private readonly settingsStorageService = inject(SettingsStorageService); + private readonly tokenStorageService = inject(TokenStorageService); + private readonly userService = inject(UserService); + + public constructor() { this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -164,55 +154,71 @@ export class GfHeaderComponent implements OnChanges { }); } + @HostListener('window:keydown', ['$event']) + protected openAssistantWithHotKey(event: KeyboardEvent) { + if ( + event.key === '/' && + event.target instanceof Element && + event.target?.nodeName?.toLowerCase() !== 'input' && + event.target?.nodeName?.toLowerCase() !== 'textarea' && + this.hasPermissionToAccessAssistant + ) { + this.assistantElement().setIsOpen(true); + this.assistentMenuTriggerElement().openMenu(); + + event.preventDefault(); + } + } + public ngOnChanges() { this.hasFilters = this.userService.hasFilters(); this.hasPermissionForAuthGoogle = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.enableAuthGoogle ); this.hasPermissionForAuthOidc = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.enableAuthOidc ); this.hasPermissionForAuthToken = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.enableAuthToken ); this.hasPermissionForSubscription = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.enableSubscription ); this.hasPermissionToAccessAdminControl = hasPermission( - this.user?.permissions, + this.user()?.permissions, permissions.accessAdminControl ); this.hasPermissionToAccessAssistant = hasPermission( - this.user?.permissions, + this.user()?.permissions, permissions.accessAssistant ); this.hasPermissionToAccessFearAndGreedIndex = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.enableFearAndGreedIndex ); this.hasPermissionToCreateUser = hasPermission( - this.info?.globalPermissions, + this.info()?.globalPermissions, permissions.createUserAccount ); } - public closeAssistant() { - this.assistentMenuTriggerElement?.closeMenu(); + protected closeAssistant() { + this.assistentMenuTriggerElement().closeMenu(); } - public impersonateAccount(aId: string) { + protected impersonateAccount(aId: string) { if (aId) { this.impersonationStorageService.setId(aId); } else { @@ -222,7 +228,7 @@ export class GfHeaderComponent implements OnChanges { window.location.reload(); } - public onDateRangeChange(dateRange: DateRange) { + protected onDateRangeChange(dateRange: DateRange) { this.dataService .putUserSetting({ dateRange }) .pipe(takeUntilDestroyed(this.destroyRef)) @@ -234,7 +240,7 @@ export class GfHeaderComponent implements OnChanges { }); } - public onFiltersChanged(filters: Filter[]) { + protected onFiltersChanged(filters: Filter[]) { const userSetting: UpdateUserSettingDto = {}; for (const filter of filters) { @@ -262,32 +268,33 @@ export class GfHeaderComponent implements OnChanges { }); } - public onLogoClick() { - if (['home', 'zen'].includes(this.currentRoute)) { + protected onLogoClick() { + if (['home', 'zen'].includes(this.currentRoute())) { this.layoutService.getShouldReloadSubject().next(); } } - public onMenuClosed() { + protected onMenuClosed() { this.isMenuOpen = false; } - public onMenuOpened() { + protected onMenuOpened() { this.isMenuOpen = true; } - public onOpenAssistant() { - this.assistantElement.initialize(); + protected onOpenAssistant() { + this.assistantElement().initialize(); } - public onSignOut() { - this.signOut.next(); + protected onSignOut() { + this.signOut.emit(); } - public openLoginDialog() { + protected openLoginDialog() { const dialogRef = this.dialog.open< GfLoginWithAccessTokenDialogComponent, - LoginWithAccessTokenDialogParams + LoginWithAccessTokenDialogParams, + LoginWithAccessTokenDialogResult >(GfLoginWithAccessTokenDialogComponent, { autoFocus: false, data: { @@ -324,7 +331,7 @@ export class GfHeaderComponent implements OnChanges { }); } - public setToken(aToken: string) { + private setToken(aToken: string) { this.tokenStorageService.saveToken( aToken, this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true' diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 13ded73eb..8c42e37ea 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -1,5 +1,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { + DEFAULT_PAGE_SIZE, NUMERICAL_PRECISION_THRESHOLD_3_FIGURES, NUMERICAL_PRECISION_THRESHOLD_5_FIGURES, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES @@ -29,7 +30,6 @@ import { DataService } from '@ghostfolio/ui/services'; import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector'; import { GfValueComponent } from '@ghostfolio/ui/value'; -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -49,6 +49,7 @@ import { MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { PageEvent } from '@angular/material/paginator'; import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -76,7 +77,6 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-flex flex-column h-100' }, imports: [ - CommonModule, GfAccountsTableComponent, GfActivitiesTableComponent, GfDataProviderCreditsComponent, @@ -142,6 +142,8 @@ export class GfHoldingDetailDialogComponent implements OnInit { public netPerformancePercentWithCurrencyEffectPrecision = 2; public netPerformanceWithCurrencyEffect: number; public netPerformanceWithCurrencyEffectPrecision = 2; + public pageIndex = 0; + public pageSize = DEFAULT_PAGE_SIZE; public quantity: number; public quantityPrecision = 2; public reportDataGlitchMail: string; @@ -180,10 +182,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { } public ngOnInit() { - const filters: Filter[] = [ - { id: this.data.dataSource, type: 'DATA_SOURCE' }, - { id: this.data.symbol, type: 'SYMBOL' } - ]; + const filters = this.getActivityFilters(); this.holdingForm = this.formBuilder.group({ tags: [] as string[] @@ -242,18 +241,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.changeDetectorRef.markForCheck(); }); - this.dataService - .fetchActivities({ - filters, - sortColumn: this.sortColumn, - sortDirection: this.sortDirection - }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ activities }) => { - this.dataSource = new MatTableDataSource(activities); - - this.changeDetectorRef.markForCheck(); - }); + this.fetchActivities(filters); this.dataService .fetchHoldingDetail({ @@ -545,6 +533,12 @@ export class GfHoldingDetailDialogComponent implements OnInit { }); } + public onChangePage(page: PageEvent) { + this.pageIndex = page.pageIndex; + + this.fetchActivities(); + } + public onCloneActivity(aActivity: Activity) { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink, @@ -628,6 +622,23 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.dialogRef.close(); } + private fetchActivities(filters: Filter[] = this.getActivityFilters()) { + this.dataService + .fetchActivities({ + filters, + skip: this.pageIndex * this.pageSize, + sortColumn: this.sortColumn, + sortDirection: this.sortDirection, + take: this.pageSize + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ activities }) => { + this.dataSource = new MatTableDataSource(activities); + + this.changeDetectorRef.markForCheck(); + }); + } + private fetchMarketData() { this.dataService .fetchMarketDataBySymbol({ @@ -650,4 +661,11 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.changeDetectorRef.markForCheck(); }); } + + private getActivityFilters(): Filter[] { + return [ + { id: this.data.dataSource, type: 'DATA_SOURCE' }, + { id: this.data.symbol, type: 'SYMBOL' } + ]; + } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 19d938235..4b04a0986 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -117,13 +117,12 @@ @@ -436,11 +437,10 @@ mat-stroked-button (click)="onCloseHolding()" > - Close Holding + Close Holding } @if ( 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 61a8935eb..661e21163 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 @@ -14,7 +14,6 @@ import { DataService } from '@ghostfolio/ui/services'; import { GfToggleComponent } from '@ghostfolio/ui/toggle'; import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart'; -import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -34,7 +33,6 @@ import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ imports: [ - CommonModule, FormsModule, GfHoldingsTableComponent, GfToggleComponent, diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index 175c88606..c44df40b3 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -44,7 +44,7 @@ (treemapChartClicked)="onHoldingClicked($event)" /> } -
    +
    ([]); + protected readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + protected readonly fearAndGreedIndex = signal(undefined); + protected readonly fearLabel = $localize`Fear`; + protected readonly greedLabel = $localize`Greed`; + protected hasPermissionToAccessFearAndGreedIndex: boolean; + protected readonly historicalDataItems = signal([]); + protected readonly numberOfDays = 365; + protected user: User; - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private userService: UserService - ) { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; + private readonly info: InfoItem; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly userService = inject(UserService); + + public constructor() { this.info = this.dataService.fetchInfo(); this.userService.stateChanged @@ -73,7 +78,10 @@ export class GfHomeMarketComponent implements OnInit { permissions.enableFearAndGreedIndex ); - if (this.hasPermissionToAccessFearAndGreedIndex) { + if ( + this.hasPermissionToAccessFearAndGreedIndex && + this.info.fearAndGreedDataSource + ) { this.dataService .fetchSymbolItem({ dataSource: this.info.fearAndGreedDataSource, @@ -82,16 +90,14 @@ export class GfHomeMarketComponent implements OnInit { }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ historicalData, marketPrice }) => { - this.fearAndGreedIndex = marketPrice; - this.historicalDataItems = [ + this.fearAndGreedIndex.set(marketPrice); + this.historicalDataItems.set([ ...historicalData, { date: resetHours(new Date()).toISOString(), value: marketPrice } - ]; - - this.changeDetectorRef.markForCheck(); + ]); }); } @@ -99,9 +105,7 @@ export class GfHomeMarketComponent implements OnInit { .fetchBenchmarks() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ benchmarks }) => { - this.benchmarks = benchmarks; - - this.changeDetectorRef.markForCheck(); + this.benchmarks.set(benchmarks); }); } } diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index fc7230d35..a782526ee 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -10,7 +10,7 @@ class="mb-3" label="Fear & Greed Index" [colorScheme]="user?.settings?.colorScheme" - [historicalDataItems]="historicalDataItems" + [historicalDataItems]="historicalDataItems()" [isAnimated]="true" [locale]="user?.settings?.locale || undefined" [showXAxis]="true" @@ -22,7 +22,7 @@ />
    @@ -31,13 +31,13 @@
    - @if (benchmarks?.length > 0) { + @if (benchmarks()?.length > 0) {
    diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index e134ad6b1..ad3556536 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -2,7 +2,11 @@ import { GfPortfolioPerformanceComponent } from '@ghostfolio/client/components/p import { LayoutService } from '@ghostfolio/client/core/layout.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; +import { + DEFAULT_CURRENCY, + DEFAULT_DATE_RANGE, + NUMERICAL_PRECISION_THRESHOLD_6_FIGURES +} from '@ghostfolio/common/config'; import { AssetProfileIdentifier, LineChartItem, @@ -14,13 +18,14 @@ import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { DataService } from '@ghostfolio/ui/services'; -import { CommonModule } from '@angular/common'; import { - ChangeDetectorRef, + ChangeDetectionStrategy, Component, - CUSTOM_ELEMENTS_SCHEMA, + computed, DestroyRef, - OnInit + inject, + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; @@ -28,80 +33,80 @@ import { RouterModule } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, GfLineChartComponent, GfPortfolioPerformanceComponent, MatButtonModule, RouterModule ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-home-overview', styleUrls: ['./home-overview.scss'], templateUrl: './home-overview.html' }) export class GfHomeOverviewComponent implements OnInit { - public deviceType: string; - public errors: AssetProfileIdentifier[]; - public hasError: boolean; - public hasImpersonationId: boolean; - public hasPermissionToCreateActivity: boolean; - public historicalDataItems: LineChartItem[]; - public isAllTimeHigh: boolean; - public isAllTimeLow: boolean; - public isLoadingPerformance = true; - public performance: PortfolioPerformance; - public performanceLabel = $localize`Performance`; - public precision = 2; - public routerLinkAccounts = internalRoutes.accounts.routerLink; - public routerLinkPortfolio = internalRoutes.portfolio.routerLink; - public routerLinkPortfolioActivities = + protected readonly errors = signal([]); + protected readonly hasImpersonationId = signal(false); + protected readonly historicalDataItems = signal(null); + protected readonly isLoadingPerformance = signal(true); + protected readonly performance = signal(null); + protected readonly performanceLabel = $localize`Performance`; + protected readonly precision = signal(2); + protected readonly user = signal(null); + + protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; + protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink; + protected readonly routerLinkPortfolioActivities = internalRoutes.portfolio.subRoutes.activities.routerLink; - public showDetails = false; - public unit: string; - public user: User; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private impersonationStorageService: ImpersonationStorageService, - private layoutService: LayoutService, - private userService: UserService - ) { + + protected readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + + protected readonly hasPermissionToCreateActivity = computed(() => { + return hasPermission(this.user()?.permissions, permissions.createActivity); + }); + + protected readonly showDetails = computed(() => { + const user = this.user(); + + return user + ? !user.settings.isRestrictedView && user.settings.viewMode !== 'ZEN' + : false; + }); + + protected readonly unit = computed(() => { + return this.showDetails() + ? (this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY) + : '%'; + }); + + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly layoutService = inject(LayoutService); + private readonly userService = inject(UserService); + + public constructor() { this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { - this.user = state.user; - - this.hasPermissionToCreateActivity = hasPermission( - this.user.permissions, - permissions.createActivity - ); - + this.user.set(state.user); this.update(); } }); } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - - this.showDetails = - !this.user.settings.isRestrictedView && - this.user.settings.viewMode !== 'ZEN'; - - this.unit = this.showDetails ? this.user.settings.baseCurrency : '%'; - this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { - this.hasImpersonationId = !!impersonationId; - - this.changeDetectorRef.markForCheck(); + this.hasImpersonationId.set(!!impersonationId); }); this.layoutService.shouldReloadContent$ @@ -112,40 +117,40 @@ export class GfHomeOverviewComponent implements OnInit { } private update() { - this.historicalDataItems = null; - this.isLoadingPerformance = true; + this.historicalDataItems.set(null); + this.isLoadingPerformance.set(true); this.dataService .fetchPortfolioPerformance({ - range: this.user?.settings?.dateRange + range: this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ chart, errors, performance }) => { - this.errors = errors; - this.performance = performance; - - this.historicalDataItems = chart.map( - ({ date, netPerformanceInPercentageWithCurrencyEffect }) => { - return { - date, - value: netPerformanceInPercentageWithCurrencyEffect * 100 - }; - } + this.errors.set(errors ?? []); + this.performance.set(performance); + + this.historicalDataItems.set( + chart?.map( + ({ date, netPerformanceInPercentageWithCurrencyEffect }) => { + return { + date, + value: (netPerformanceInPercentageWithCurrencyEffect ?? 0) * 100 + }; + } + ) ?? null ); + this.precision.set(2); + if ( - this.deviceType === 'mobile' && - this.performance.currentValueInBaseCurrency >= + this.deviceType() === 'mobile' && + performance.currentValueInBaseCurrency >= NUMERICAL_PRECISION_THRESHOLD_6_FIGURES ) { - this.precision = 0; + this.precision.set(0); } - this.isLoadingPerformance = false; - - this.changeDetectorRef.markForCheck(); + this.isLoadingPerformance.set(false); }); - - this.changeDetectorRef.markForCheck(); } } diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index b36302ded..8361c5b88 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -2,19 +2,16 @@ class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative" > @if ( - !hasImpersonationId && - hasPermissionToCreateActivity && - user?.activitiesCount === 0 + !hasImpersonationId() && + hasPermissionToCreateActivity() && + user()?.activitiesCount === 0 ) {
    diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index 60960480d..a63876a54 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -1,27 +1,27 @@ import { GfPortfolioSummaryComponent } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.component'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { - InfoItem, - PortfolioSummary, - User -} from '@ghostfolio/common/interfaces'; +import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataService } from '@ghostfolio/ui/services'; import { - ChangeDetectorRef, + ChangeDetectionStrategy, Component, + computed, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, - OnInit + inject, + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatCardModule } from '@angular/material/card'; -import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; import { DeviceDetectorService } from 'ngx-device-detector'; +import { switchMap } from 'rxjs'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, imports: [GfPortfolioSummaryComponent, MatCardModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-home-summary', @@ -29,87 +29,75 @@ import { DeviceDetectorService } from 'ngx-device-detector'; templateUrl: './home-summary.html' }) export class GfHomeSummaryComponent implements OnInit { - public deviceType: string; - public hasImpersonationId: boolean; - public hasPermissionForSubscription: boolean; - public hasPermissionToUpdateUserSettings: boolean; - public info: InfoItem; - public isLoading = true; - public snackBarRef: MatSnackBarRef; - public summary: PortfolioSummary; - public user: User; + protected readonly hasImpersonationId = signal(false); + protected readonly isLoading = signal(true); + protected readonly summary = signal(undefined); + protected readonly user = signal(undefined); + + protected readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + + protected readonly hasPermissionToUpdateUserSettings = computed(() => { + const user = this.user(); - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private impersonationStorageService: ImpersonationStorageService, - private userService: UserService - ) { - this.info = this.dataService.fetchInfo(); + return user + ? hasPermission(user.permissions, permissions.updateUserSettings) + : false; + }); - this.hasPermissionForSubscription = hasPermission( - this.info?.globalPermissions, - permissions.enableSubscription - ); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly userService = inject(UserService); + public constructor() { this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { - this.user = state.user; - - this.hasPermissionToUpdateUserSettings = hasPermission( - this.user.permissions, - permissions.updateUserSettings - ); - + this.user.set(state.user); this.update(); } }); } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { - this.hasImpersonationId = !!impersonationId; + this.hasImpersonationId.set(!!impersonationId); }); } - public onChangeEmergencyFund(emergencyFund: number) { + protected onChangeEmergencyFund(emergencyFund: number) { this.dataService .putUserSetting({ emergencyFund }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.userService - .get(true) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((user) => { - this.user = user; - - this.changeDetectorRef.markForCheck(); - }); + .pipe( + switchMap(() => this.userService.get(true)), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((user) => { + this.user.set(user); }); } private update() { - this.isLoading = true; + this.isLoading.set(true); this.dataService .fetchPortfolioDetails() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ summary }) => { - this.summary = summary; - this.isLoading = false; + if (summary) { + this.summary.set(summary); + } - this.changeDetectorRef.markForCheck(); + this.isLoading.set(false); }); - - this.changeDetectorRef.markForCheck(); } } diff --git a/apps/client/src/app/components/home-summary/home-summary.html b/apps/client/src/app/components/home-summary/home-summary.html index 064923a04..a6da72b03 100644 --- a/apps/client/src/app/components/home-summary/home-summary.html +++ b/apps/client/src/app/components/home-summary/home-summary.html @@ -1,21 +1,21 @@ -
    +

    Summary

    diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html index a17259000..c7c9a9c4b 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.html +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html @@ -14,6 +14,7 @@ [deviceType]="deviceType()" [hasPermissionToDeleteItem]="hasPermissionToDeleteWatchlistItem" [locale]="user?.settings?.locale || undefined" + [showIcon]="true" [user]="user" (itemDeleted)="onWatchlistItemDeleted($event)" /> diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 21a7ac85a..691133009 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -24,7 +24,7 @@ import { Input, OnChanges, OnDestroy, - ViewChild + viewChild } from '@angular/core'; import { BarController, @@ -55,20 +55,21 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; templateUrl: './investment-chart.component.html' }) export class GfInvestmentChartComponent implements OnChanges, OnDestroy { - @Input() benchmarkDataItems: InvestmentItem[] = []; - @Input() benchmarkDataLabel = ''; - @Input() colorScheme: ColorScheme; - @Input() currency: string; - @Input() groupBy: GroupBy; - @Input() historicalDataItems: LineChartItem[] = []; - @Input() isInPercentage = false; - @Input() isLoading = false; - @Input() locale = getLocale(); - @Input() savingsRate = 0; + @Input() public readonly benchmarkDataItems: InvestmentItem[] = []; + @Input() public readonly benchmarkDataLabel = ''; + @Input() public readonly colorScheme: ColorScheme; + @Input() public readonly currency: string; + @Input() public readonly groupBy: GroupBy; + @Input() public readonly historicalDataItems: LineChartItem[] = []; + @Input() public readonly isInPercentage = false; + @Input() public readonly isLoading = false; + @Input() public readonly locale = getLocale(); + @Input() public readonly savingsRate = 0; - @ViewChild('chartCanvas') chartCanvas: ElementRef; + private readonly chartCanvas = + viewChild.required>('chartCanvas'); - public chart: Chart<'bar' | 'line'>; + private chart: Chart<'bar' | 'line'>; private investments: InvestmentItem[]; private values: LineChartItem[]; @@ -118,7 +119,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { borderWidth: this.groupBy ? 0 : 1, data: this.investments.map(({ date, investment }) => { return { - x: parseDate(date).getTime(), + x: parseDate(date)?.getTime() ?? null, y: this.isInPercentage ? investment * 100 : investment }; }), @@ -138,7 +139,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { borderWidth: 2, data: this.values.map(({ date, value }) => { return { - x: parseDate(date).getTime(), + x: parseDate(date)?.getTime() ?? null, y: this.isInPercentage ? value * 100 : value }; }), @@ -165,123 +166,126 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { this.getTooltipPluginConfiguration(); const annotations = this.chart.options.plugins.annotation - .annotations as Record>; + ?.annotations as Record>; if (this.savingsRate && annotations.savingsRate) { annotations.savingsRate.value = this.savingsRate; } this.chart.update(); } else { - this.chart = new Chart(this.chartCanvas.nativeElement, { - data: chartData, - options: { - animation: false, - elements: { - line: { - tension: 0 + this.chart = new Chart<'bar' | 'line'>( + this.chartCanvas().nativeElement, + { + data: chartData, + options: { + animation: false, + elements: { + line: { + tension: 0 + }, + point: { + hoverBackgroundColor: getBackgroundColor(this.colorScheme), + hoverRadius: 2, + radius: 0 + } }, - point: { - hoverBackgroundColor: getBackgroundColor(this.colorScheme), - hoverRadius: 2, - radius: 0 - } - }, - interaction: { intersect: false, mode: 'index' }, - maintainAspectRatio: true, - plugins: { - annotation: { - annotations: { - savingsRate: this.savingsRate - ? { - borderColor: `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.75)`, - borderWidth: 1, - label: { - backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, - borderRadius: 2, - color: 'white', - content: $localize`Savings Rate`, - display: true, - font: { size: 10, weight: 'normal' }, - padding: { - x: 4, - y: 2 + interaction: { intersect: false, mode: 'index' }, + maintainAspectRatio: true, + plugins: { + annotation: { + annotations: { + savingsRate: this.savingsRate + ? { + borderColor: `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.75)`, + borderWidth: 1, + label: { + backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, + borderRadius: 2, + color: 'white', + content: $localize`Savings Rate`, + display: true, + font: { size: 10, weight: 'normal' }, + padding: { + x: 4, + y: 2 + }, + position: 'start' }, - position: 'start' - }, - scaleID: 'y', - type: 'line', - value: this.savingsRate - } - : undefined, - yAxis: { - borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, - borderWidth: 1, - scaleID: 'y', - type: 'line', - value: 0 + scaleID: 'y', + type: 'line', + value: this.savingsRate + } + : undefined, + yAxis: { + borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, + borderWidth: 1, + scaleID: 'y', + type: 'line', + value: 0 + } } - } - }, - legend: { - display: false - }, - tooltip: this.getTooltipPluginConfiguration(), - verticalHoverLine: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` - } - }, - responsive: true, - scales: { - x: { - border: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, - width: this.groupBy ? 0 : 1 }, - display: true, - grid: { + legend: { display: false }, - type: 'time', - time: { - tooltipFormat: getDateFormatString(this.locale), - unit: 'year' + tooltip: this.getTooltipPluginConfiguration(), + verticalHoverLine: { + color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` } }, - y: { - border: { - display: false - }, - display: !this.isInPercentage, - grid: { - color: ({ scale, tick }) => { - if ( - tick.value === 0 || - tick.value === scale.max || - tick.value === scale.min - ) { - return `rgba(${getTextColor(this.colorScheme)}, 0.1)`; - } - - return 'transparent'; + responsive: true, + scales: { + x: { + border: { + color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, + width: this.groupBy ? 0 : 1 + }, + display: true, + grid: { + display: false + }, + type: 'time', + time: { + tooltipFormat: getDateFormatString(this.locale), + unit: 'year' } }, - position: 'right', - ticks: { - callback: (value: number) => { - return transformTickToAbbreviation(value); + y: { + border: { + display: false }, - display: true, - mirror: true, - z: 1 + display: !this.isInPercentage, + grid: { + color: ({ scale, tick }) => { + if ( + tick.value === 0 || + tick.value === scale.max || + tick.value === scale.min + ) { + return `rgba(${getTextColor(this.colorScheme)}, 0.1)`; + } + + return 'transparent'; + } + }, + position: 'right', + ticks: { + callback: (value: number) => { + return transformTickToAbbreviation(value); + }, + display: true, + mirror: true, + z: 1 + } } } - } - }, - plugins: [ - getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme) - ], - type: this.groupBy ? 'bar' : 'line' - }); + }, + plugins: [ + getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme) + ], + type: this.groupBy ? 'bar' : 'line' + } + ); } } } @@ -305,8 +309,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { } private isInFuture(aContext: ScriptableLineSegmentContext, aValue: T) { - return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) - ? aValue - : undefined; + const xValue = aContext?.p1?.parsed?.x; + + if (xValue == null) { + return undefined; + } + + return isAfter(new Date(xValue), new Date()) ? aValue : undefined; } } diff --git a/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts index e9222e142..b903fcfef 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/login-with-access-token-dialog/interfaces/interfaces.ts @@ -5,3 +5,7 @@ export interface LoginWithAccessTokenDialogParams { hasPermissionToUseAuthToken: boolean; title: string; } + +export interface LoginWithAccessTokenDialogResult { + accessToken: string | null; +} diff --git a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts index d79c7a675..d6ce28c96 100644 --- a/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts +++ b/apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts @@ -22,7 +22,10 @@ import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { eyeOffOutline, eyeOutline } from 'ionicons/icons'; -import { LoginWithAccessTokenDialogParams } from './interfaces/interfaces'; +import { + LoginWithAccessTokenDialogParams, + LoginWithAccessTokenDialogResult +} from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -49,7 +52,10 @@ export class GfLoginWithAccessTokenDialogComponent { public constructor( @Inject(MAT_DIALOG_DATA) public data: LoginWithAccessTokenDialogParams, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef< + GfLoginWithAccessTokenDialogComponent, + LoginWithAccessTokenDialogResult + >, private settingsStorageService: SettingsStorageService ) { addIcons({ eyeOffOutline, eyeOutline }); diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index a2c5c17eb..e8bcfea0e 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -25,10 +25,6 @@
    diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 04c0f507c..56e75ec1e 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -10,7 +10,6 @@ import { import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfValueComponent } from '@ghostfolio/ui/value'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -28,7 +27,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, GfValueComponent, IonIcon, NgxSkeletonLoaderModule], + imports: [GfValueComponent, IonIcon, NgxSkeletonLoaderModule], selector: 'gf-portfolio-performance', styleUrls: ['./portfolio-performance.component.scss'], templateUrl: './portfolio-performance.component.html' @@ -36,8 +35,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; export class GfPortfolioPerformanceComponent implements OnChanges { @Input() deviceType: string; @Input() errors: ResponseError['errors']; - @Input() isAllTimeHigh: boolean; - @Input() isAllTimeLow: boolean; @Input() isLoading: boolean; @Input() locale = getLocale(); @Input() performance: PortfolioPerformance; diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 0e26a49a8..e14479425 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -181,12 +181,11 @@
    +
    Threshold Min:
    -
    +
    Threshold Max: + public dialogRef: MatDialogRef< + GfUserDetailDialogComponent, + UserDetailDialogResult + > ) { addIcons({ ellipsisVertical 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 b23377f3c..616977d80 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -1,20 +1,15 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; +import { + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { - ChangeDetectorRef, - Component, - CUSTOM_ELEMENTS_SCHEMA, - DestroyRef, - OnInit -} from '@angular/core'; +import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { documentTextOutline, @@ -24,18 +19,15 @@ import { shieldCheckmarkOutline, sparklesOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-about-page', styleUrls: ['./about-page.scss'], templateUrl: './about-page.html' }) -export class AboutPageComponent implements OnInit { - public deviceType: string; +export class AboutPageComponent { public hasPermissionForSubscription: boolean; public tabs: TabConfiguration[] = []; public user: User; @@ -44,7 +36,6 @@ export class AboutPageComponent implements OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, private userService: UserService ) { const { globalPermissions } = this.dataService.fetchInfo(); @@ -112,8 +103,4 @@ export class AboutPageComponent implements OnInit { sparklesOutline }); } - - public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - } } diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -1,30 +1 @@ - - - - - + diff --git a/apps/client/src/app/pages/about/oss-friends/oss-friends-page.html b/apps/client/src/app/pages/about/oss-friends/oss-friends-page.html index f1431697f..48985eaa3 100644 --- a/apps/client/src/app/pages/about/oss-friends/oss-friends-page.html +++ b/apps/client/src/app/pages/about/oss-friends/oss-friends-page.html @@ -1,5 +1,5 @@
    -
    +

    @for (ossFriend of ossFriends; track ossFriend) { -
    +
    diff --git a/apps/client/src/app/pages/about/overview/about-overview-page.component.ts b/apps/client/src/app/pages/about/overview/about-overview-page.component.ts index 9c399762b..92a98598b 100644 --- a/apps/client/src/app/pages/about/overview/about-overview-page.component.ts +++ b/apps/client/src/app/pages/about/overview/about-overview-page.component.ts @@ -4,7 +4,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { DataService } from '@ghostfolio/ui/services'; -import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -26,7 +25,7 @@ import { } from 'ionicons/icons'; @Component({ - imports: [CommonModule, IonIcon, MatButtonModule, RouterModule], + imports: [IonIcon, MatButtonModule, RouterModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-about-overview-page', styleUrls: ['./about-overview-page.scss'], diff --git a/apps/client/src/app/pages/about/overview/about-overview-page.html b/apps/client/src/app/pages/about/overview/about-overview-page.html index cab1caa23..c28a63be2 100644 --- a/apps/client/src/app/pages/about/overview/about-overview-page.html +++ b/apps/client/src/app/pages/about/overview/about-overview-page.html @@ -178,7 +178,7 @@
    -
    +
    Platform - - - - + diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts index 357a08bbd..e75a51c73 100644 --- a/apps/client/src/app/pages/api/api-page.component.ts +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -4,6 +4,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { + AiServiceHealthResponse, DataProviderGhostfolioAssetProfileResponse, DataProviderGhostfolioStatusResponse, DividendsResponse, @@ -13,27 +14,42 @@ import { } from '@ghostfolio/common/interfaces'; import { CommonModule } from '@angular/common'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpParams +} from '@angular/common/http'; import { Component, DestroyRef, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatCardModule } from '@angular/material/card'; import { format, startOfYear } from 'date-fns'; -import { map, Observable } from 'rxjs'; +import { isObject } from 'lodash'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { catchError, map, Observable, of, OperatorFunction } from 'rxjs'; + +import { FetchFailure, FetchResult } from './interfaces/interfaces'; @Component({ host: { class: 'page' }, - imports: [CommonModule], + imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule], selector: 'gf-api-page', styleUrls: ['./api-page.scss'], templateUrl: './api-page.html' }) export class GfApiPageComponent implements OnInit { - public assetProfile$: Observable; - public dividends$: Observable; - public historicalData$: Observable; - public isinLookupItems$: Observable; - public lookupItems$: Observable; - public quotes$: Observable; - public status$: Observable; + public aiServiceHealth$: Observable>; + public assetProfile$: Observable< + FetchResult + >; + public dividends$: Observable>; + public historicalData$: Observable< + FetchResult + >; + public isinLookupItems$: Observable>; + public lookupItems$: Observable>; + public quotes$: Observable>; + public status$: Observable>; private apiKey: string; @@ -45,6 +61,7 @@ export class GfApiPageComponent implements OnInit { public ngOnInit() { this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`); + this.aiServiceHealth$ = this.fetchAiServiceHealth(); this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' }); this.dividends$ = this.fetchDividends({ symbol: 'KO' }); this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' }); @@ -54,13 +71,33 @@ export class GfApiPageComponent implements OnInit { this.status$ = this.fetchStatus(); } + public isFetchFailure(value: unknown): value is FetchFailure { + return isObject(value) && value !== null && 'fetchError' in value; + } + + private catchFetchFailure(): OperatorFunction { + return catchError(({ error }: HttpErrorResponse) => { + const body = error as { message?: string; status?: string }; + const status = body?.status ?? 'Error'; + const fetchError = body?.message ? `${status}: ${body.message}` : status; + + return of({ fetchError }); + }) as OperatorFunction; + } + + private fetchAiServiceHealth() { + return this.http + .get('/api/v1/health/ai') + .pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef)); + } + private fetchAssetProfile({ symbol }: { symbol: string }) { return this.http .get( `/api/v1/data-providers/ghostfolio/asset-profile/${symbol}`, { headers: this.getHeaders() } ) - .pipe(takeUntilDestroyed(this.destroyRef)); + .pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef)); } private fetchDividends({ symbol }: { symbol: string }) { @@ -80,6 +117,7 @@ export class GfApiPageComponent implements OnInit { map(({ dividends }) => { return dividends; }), + this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef) ); } @@ -101,6 +139,7 @@ export class GfApiPageComponent implements OnInit { map(({ historicalData }) => { return historicalData; }), + this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef) ); } @@ -127,6 +166,7 @@ export class GfApiPageComponent implements OnInit { map(({ items }) => { return items; }), + this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef) ); } @@ -143,6 +183,7 @@ export class GfApiPageComponent implements OnInit { map(({ quotes }) => { return quotes; }), + this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef) ); } @@ -153,7 +194,7 @@ export class GfApiPageComponent implements OnInit { '/api/v2/data-providers/ghostfolio/status', { headers: this.getHeaders() } ) - .pipe(takeUntilDestroyed(this.destroyRef)); + .pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef)); } private getHeaders() { diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html index 3c43484e6..07f5ec981 100644 --- a/apps/client/src/app/pages/api/api-page.html +++ b/apps/client/src/app/pages/api/api-page.html @@ -1,77 +1,173 @@
    -
    -

    Status

    -
    {{ status$ | async | json }}
    -
    -
    -

    Asset Profile

    -
    {{ assetProfile$ | async | json }}
    -
    -
    -

    Lookup

    - @if (lookupItems$) { - @let symbols = lookupItems$ | async; -
      - @for (item of symbols; track item.symbol) { -
    • {{ item.name }} ({{ item.symbol }})
    • - } -
    - } -
    -
    -

    Lookup (ISIN)

    - @if (isinLookupItems$) { - @let symbols = isinLookupItems$ | async; -
      - @for (item of symbols; track item.symbol) { -
    • {{ item.name }} ({{ item.symbol }})
    • - } -
    - } -
    -
    -

    Quotes

    - @if (quotes$) { - @let quotes = quotes$ | async; -
      - @for (quote of quotes | keyvalue; track quote) { -
    • - {{ quote.key }}: {{ quote.value.marketPrice }} - {{ quote.value.currency }} -
    • - } -
    - } -
    -
    -

    Historical

    - @if (historicalData$) { - @let historicalData = historicalData$ | async; -
      - @for ( - historicalDataItem of historicalData | keyvalue; - track historicalDataItem - ) { -
    • - {{ historicalDataItem.key }}: - {{ historicalDataItem.value.marketPrice }} -
    • - } -
    - } -
    -
    -

    Dividends

    - @if (dividends$) { - @let dividends = dividends$ | async; -
      - @for (dividend of dividends | keyvalue; track dividend) { -
    • - {{ dividend.key }}: - {{ dividend.value.marketPrice }} -
    • - } -
    - } +
    +
    + + + AI Service Health + + + @let aiServiceHealth = aiServiceHealth$ | async; + @if (isFetchFailure(aiServiceHealth)) { + 🔴 {{ aiServiceHealth.fetchError }} + } @else if (aiServiceHealth) { + 🟢 {{ aiServiceHealth.status }} + } @else { + + } + + + + + Status + + + @let status = status$ | async; + @if (isFetchFailure(status)) { + 🔴 {{ status.fetchError }} + } @else if (status) { +
    {{ status | json }}
    + } @else { + + } +
    +
    + + + Asset Profile + + + @let assetProfile = assetProfile$ | async; + @if (isFetchFailure(assetProfile)) { + 🔴 {{ assetProfile.fetchError }} + } @else if (assetProfile) { +
    {{ assetProfile | json }}
    + } @else { + + } +
    +
    + + + Lookup + + + @let symbols = lookupItems$ | async; + @if (isFetchFailure(symbols)) { + 🔴 {{ symbols.fetchError }} + } @else if (symbols) { +
      + @for (item of symbols; track item.symbol) { +
    • {{ item.name }} ({{ item.symbol }})
    • + } +
    + } @else { + + } +
    +
    + + + Lookup (ISIN) + + + @let isinSymbols = isinLookupItems$ | async; + @if (isFetchFailure(isinSymbols)) { + 🔴 {{ isinSymbols.fetchError }} + } @else if (isinSymbols) { +
      + @for (item of isinSymbols; track item.symbol) { +
    • {{ item.name }} ({{ item.symbol }})
    • + } +
    + } @else { + + } +
    +
    + + + Quotes + + + @let quotes = quotes$ | async; + @if (isFetchFailure(quotes)) { + 🔴 {{ quotes.fetchError }} + } @else if (quotes) { +
      + @for (quote of quotes | keyvalue; track quote) { +
    • + {{ quote.key }}: {{ quote.value.marketPrice }} + {{ quote.value.currency }} +
    • + } +
    + } @else { + + } +
    +
    + + + Historical + + + @let historicalData = historicalData$ | async; + @if (isFetchFailure(historicalData)) { + 🔴 {{ historicalData.fetchError }} + } @else if (historicalData) { +
      + @for (item of historicalData | keyvalue; track item) { +
    • {{ item.key }}: {{ item.value.marketPrice }}
    • + } +
    + } @else { + + } +
    +
    + + + Dividends + + + @let dividends = dividends$ | async; + @if (isFetchFailure(dividends)) { + 🔴 {{ dividends.fetchError }} + } @else if (dividends) { +
      + @for (dividend of dividends | keyvalue; track dividend) { +
    • {{ dividend.key }}: {{ dividend.value.marketPrice }}
    • + } +
    + } @else { + + } +
    +
    +
    diff --git a/apps/client/src/app/pages/api/interfaces/interfaces.ts b/apps/client/src/app/pages/api/interfaces/interfaces.ts new file mode 100644 index 000000000..07de2b2f7 --- /dev/null +++ b/apps/client/src/app/pages/api/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +export interface FetchFailure { + fetchError: string; +} + +export type FetchResult = T | FetchFailure; 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 8ae074bee..60081687a 100644 --- a/apps/client/src/app/pages/faq/faq-page.component.ts +++ b/apps/client/src/app/pages/faq/faq-page.component.ts @@ -1,33 +1,27 @@ -import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; +import { + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; import { DataService } from '@ghostfolio/ui/services'; -import { CUSTOM_ELEMENTS_SCHEMA, Component, OnInit } from '@angular/core'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; +import { Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-faq-page', styleUrls: ['./faq-page.scss'], templateUrl: './faq-page.html' }) -export class GfFaqPageComponent implements OnInit { - public deviceType: string; +export class GfFaqPageComponent { public hasPermissionForSubscription: boolean; public tabs: TabConfiguration[] = []; - public constructor( - private dataService: DataService, - private deviceDetectorService: DeviceDetectorService - ) { + public constructor(private dataService: DataService) { const { globalPermissions } = this.dataService.fetchInfo(); this.hasPermissionForSubscription = hasPermission( @@ -56,8 +50,4 @@ export class GfFaqPageComponent implements OnInit { addIcons({ cloudyOutline, readerOutline, serverOutline }); } - - public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - } } diff --git a/apps/client/src/app/pages/faq/faq-page.html b/apps/client/src/app/pages/faq/faq-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/faq/faq-page.html +++ b/apps/client/src/app/pages/faq/faq-page.html @@ -1,30 +1 @@ - - - - - + 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 958428331..8c5caab22 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -1,20 +1,20 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; import { ChangeDetectorRef, Component, - CUSTOM_ELEMENTS_SCHEMA, DestroyRef, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { albumsOutline, @@ -23,18 +23,15 @@ import { newspaperOutline, readerOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-home-page', styleUrls: ['./home-page.scss'], templateUrl: './home-page.html' }) export class GfHomePageComponent implements OnInit { - public deviceType: string; public hasImpersonationId: boolean; public tabs: TabConfiguration[] = []; public user: User; @@ -42,7 +39,6 @@ export class GfHomePageComponent implements OnInit { public constructor( private changeDetectorRef: ChangeDetectorRef, private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, private userService: UserService ) { @@ -104,8 +100,6 @@ export class GfHomePageComponent implements OnInit { } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/apps/client/src/app/pages/home/home-page.html b/apps/client/src/app/pages/home/home-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/home/home-page.html +++ b/apps/client/src/app/pages/home/home-page.html @@ -1,30 +1 @@ - - - - - + diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts index 2255881c3..386992a6e 100644 --- a/apps/client/src/app/pages/landing/landing-page.component.ts +++ b/apps/client/src/app/pages/landing/landing-page.component.ts @@ -8,7 +8,6 @@ import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart'; -import { CommonModule } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; @@ -25,7 +24,6 @@ import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ host: { class: 'page' }, imports: [ - CommonModule, GfCarouselComponent, GfLogoCarouselComponent, GfLogoComponent, diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index 50602858a..dc0df10d4 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -54,7 +54,7 @@
    -
    +
    Account @@ -103,18 +104,16 @@
    -
    +
    Update Cash Balance
    Name, symbol or ISIN @@ -127,9 +126,9 @@
    Name @@ -173,13 +172,12 @@
    Quantity @@ -188,7 +186,7 @@
    @@ -215,7 +213,7 @@
    @for (currency of currencies; track currency) { @@ -247,12 +245,11 @@
    Fee @@ -260,7 +257,7 @@
    {{ activityForm.get('currencyOfUnitPrice')?.value }}
    @@ -280,7 +277,7 @@
    Asset Class @@ -299,7 +296,7 @@
    Asset Sub Class 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 10df36857..42260d648 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 @@ -1,5 +1,6 @@ import { GfFileDropDirective } from '@ghostfolio/client/directives/file-drop/file-drop.directive'; import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; +import { DEFAULT_DATE_RANGE } from '@ghostfolio/common/config'; import { CreateAccountWithBalancesDto, CreateAssetProfileWithMarketDataDto, @@ -22,11 +23,11 @@ import { ChangeDetectorRef, Component, DestroyRef, - Inject + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { - FormBuilder, + FormControl, FormGroup, ReactiveFormsModule, Validators @@ -52,7 +53,6 @@ import { addIcons } from 'ionicons'; import { cloudUploadOutline, warningOutline } from 'ionicons/icons'; import { isArray, sortBy } from 'lodash'; import ms from 'ms'; -import { DeviceDetectorService } from 'ngx-device-detector'; import { ImportStep } from './enums/import-step'; import { ImportActivitiesDialogParams } from './interfaces/interfaces'; @@ -82,51 +82,48 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; templateUrl: 'import-activities-dialog.html' }) export class GfImportActivitiesDialogComponent { - public accounts: CreateAccountWithBalancesDto[] = []; - public activities: Activity[] = []; - public assetProfileForm: FormGroup; - public assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; - public dataSource: MatTableDataSource; - public details: any[] = []; - public deviceType: string; - public dialogTitle = $localize`Import Activities`; - public errorMessages: string[] = []; - public holdings: PortfolioPosition[] = []; - public importStep: ImportStep = ImportStep.UPLOAD_FILE; - public isLoading = false; - public mode: 'DIVIDEND'; - public pageIndex = 0; - public pageSize = 8; - public selectedActivities: Activity[] = []; - public sortColumn = 'date'; - public sortDirection: SortDirection = 'desc'; - public stepperOrientation: StepperOrientation; - public tags: CreateTagDto[] = []; - public totalItems: number; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private formBuilder: FormBuilder, - public dialogRef: MatDialogRef, - private importActivitiesService: ImportActivitiesService, - private snackBar: MatSnackBar - ) { + protected readonly assetProfileForm = new FormGroup({ + assetProfileIdentifier: new FormControl(null, { + validators: [Validators.required] + }) + }); + protected readonly data = + inject(MAT_DIALOG_DATA); + protected dataSource: MatTableDataSource; + protected details: any[] = []; + protected dialogTitle = $localize`Import Activities`; + protected errorMessages: string[] = []; + protected holdings: PortfolioPosition[] = []; + protected importStep: ImportStep = ImportStep.UPLOAD_FILE; + protected isLoading = false; + protected mode: 'DIVIDEND'; + protected pageIndex = 0; + protected readonly pageSize = 8; + protected selectedActivities: Activity[] = []; + protected readonly sortColumn = 'date'; + protected readonly sortDirection: SortDirection = 'desc'; + protected readonly stepperOrientation: StepperOrientation = + this.data.deviceType === 'mobile' ? 'vertical' : 'horizontal'; + protected totalItems: number; + + private accounts: CreateAccountWithBalancesDto[] = []; + private activities: Activity[] = []; + private assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + private tags: CreateTagDto[] = []; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly dialogRef = + inject>(MatDialogRef); + private readonly importActivitiesService = inject(ImportActivitiesService); + private readonly snackBar = inject(MatSnackBar); + + public constructor() { addIcons({ cloudUploadOutline, warningOutline }); } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.stepperOrientation = - this.deviceType === 'mobile' ? 'vertical' : 'horizontal'; - - this.assetProfileForm = this.formBuilder.group({ - assetProfileIdentifier: [undefined, Validators.required] - }); - if ( this.data?.activityTypes?.length === 1 && this.data?.activityTypes?.[0] === 'DIVIDEND' @@ -135,7 +132,7 @@ export class GfImportActivitiesDialogComponent { this.dialogTitle = $localize`Import Dividends`; this.mode = 'DIVIDEND'; - this.assetProfileForm.get('assetProfileIdentifier').disable(); + this.assetProfileForm.controls.assetProfileIdentifier.disable(); this.dataService .fetchPortfolioHoldings({ @@ -149,14 +146,15 @@ export class GfImportActivitiesDialogComponent { type: 'ASSET_CLASS' } ], - range: 'max' + range: DEFAULT_DATE_RANGE }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { - this.holdings = sortBy(holdings, ({ name }) => { - return name.toLowerCase(); + this.holdings = sortBy(holdings, ({ assetProfile }) => { + return assetProfile.name.toLowerCase(); }); - this.assetProfileForm.get('assetProfileIdentifier').enable(); + + this.assetProfileForm.controls.assetProfileIdentifier.enable(); this.isLoading = false; @@ -165,11 +163,11 @@ export class GfImportActivitiesDialogComponent { } } - public onCancel() { + protected onCancel() { this.dialogRef.close(); } - public async onImportActivities() { + protected async onImportActivities() { try { this.snackBar.open('⏳ ' + $localize`Importing data...`); @@ -202,7 +200,7 @@ export class GfImportActivitiesDialogComponent { } } - public onFilesDropped({ + protected onFilesDropped({ files, stepper }: { @@ -216,7 +214,7 @@ export class GfImportActivitiesDialogComponent { this.handleFile({ stepper, file: files[0] }); } - public onImportStepChange(event: StepperSelectionEvent) { + protected onImportStepChange(event: StepperSelectionEvent) { if (event.selectedIndex === ImportStep.UPLOAD_FILE) { this.importStep = ImportStep.UPLOAD_FILE; } else if (event.selectedIndex === ImportStep.SELECT_ACTIVITIES) { @@ -224,12 +222,15 @@ export class GfImportActivitiesDialogComponent { } } - public onLoadDividends(aStepper: MatStepper) { - this.assetProfileForm.get('assetProfileIdentifier').disable(); + protected onLoadDividends(aStepper: MatStepper) { + this.assetProfileForm.controls.assetProfileIdentifier.disable(); - const { dataSource, symbol } = this.assetProfileForm.get( - 'assetProfileIdentifier' - ).value; + const { dataSource, symbol } = + this.assetProfileForm.controls.assetProfileIdentifier.value ?? {}; + + if (!dataSource || !symbol) { + return; + } this.dataService .fetchDividendsImport({ @@ -249,47 +250,45 @@ export class GfImportActivitiesDialogComponent { }); } - public onPageChanged({ pageIndex }: PageEvent) { + protected onPageChanged({ pageIndex }: PageEvent) { this.pageIndex = pageIndex; } - public onReset(aStepper: MatStepper) { + protected onReset(aStepper: MatStepper) { this.details = []; this.errorMessages = []; this.importStep = ImportStep.SELECT_ACTIVITIES; this.pageIndex = 0; - this.assetProfileForm.get('assetProfileIdentifier').enable(); + this.assetProfileForm.controls.assetProfileIdentifier.enable(); aStepper.reset(); } - public onSelectFile(stepper: MatStepper) { + protected onSelectFile(stepper: MatStepper) { const input = document.createElement('input'); + input.accept = 'application/JSON, .csv'; input.type = 'file'; input.onchange = (event) => { // Getting the file reference - const file = (event.target as HTMLInputElement).files[0]; - this.handleFile({ file, stepper }); + const file = (event.target as HTMLInputElement).files?.[0]; + + if (file) { + this.handleFile({ file, stepper }); + } }; input.click(); } - public updateSelection(activities: Activity[]) { + protected updateSelection(activities: Activity[]) { this.selectedActivities = activities.filter(({ error }) => { return !error; }); } - private async handleFile({ - file, - stepper - }: { - file: File; - stepper: MatStepper; - }): Promise { + private handleFile({ file, stepper }: { file: File; stepper: MatStepper }) { this.snackBar.open('⏳ ' + $localize`Validating data...`); // Setting up the reader @@ -297,7 +296,7 @@ export class GfImportActivitiesDialogComponent { reader.readAsText(file, 'UTF-8'); reader.onload = async (readerEvent) => { - const fileContent = readerEvent.target.result as string; + const fileContent = readerEvent.target?.result as string; const fileExtension = file.name.split('.').pop()?.toLowerCase(); try { diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index 508fdd753..85fb73ba2 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -38,18 +38,18 @@ {{ holding.name }}{{ holding.assetProfile.name }}
    {{ holding.symbol | gfSymbol }} · - {{ holding.currency }}{{ holding.assetProfile.symbol | gfSymbol }} · + {{ holding.assetProfile.currency }}
    } diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index ebfd336d3..a7f8cd2ec 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -21,7 +21,6 @@ import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart'; -import { NgClass } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -51,8 +50,7 @@ import { DeviceDetectorService } from 'ngx-device-detector'; GfValueComponent, GfWorldMapChartComponent, MatCardModule, - MatProgressBarModule, - NgClass + MatProgressBarModule ], selector: 'gf-allocations-page', styleUrls: ['./allocations-page.scss'], diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 33431ce5d..a1000189b 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -319,9 +319,7 @@
    diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index cabd7fcb4..76ad6cbf6 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -62,7 +62,7 @@ />
    } @else { -
    +
    If you retire today, you would be able to withdraw - - - - + diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html index af74137d1..eb6462cfa 100644 --- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html @@ -30,7 +30,7 @@ } @else {
    @for (category of categories; track category.key) { -
    +

    {{ category.name }} @if (user?.subscription?.type === 'Basic') { diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts index 650522896..c5c4fc979 100644 --- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts @@ -12,7 +12,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { DataService } from '@ghostfolio/ui/services'; -import { NgClass } from '@angular/common'; import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IonIcon } from '@ionic/angular/standalone'; @@ -29,7 +28,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; GfPremiumIndicatorComponent, GfRulesComponent, IonIcon, - NgClass, NgxSkeletonLoaderModule ], selector: 'gf-x-ray-page', @@ -109,7 +107,10 @@ export class GfXRayPageComponent { .fetchPortfolioReport() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ xRay: { categories, statistics } }) => { - this.categories = categories; + this.categories = categories.filter(({ rules }) => { + return rules?.length > 0; + }); + this.inactiveRules = this.mergeInactiveRules(categories); this.statistics = statistics; diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 0b76fcd1d..a1fe0c0b5 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -7,7 +7,6 @@ import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { DataService } from '@ghostfolio/ui/services'; -import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -35,7 +34,6 @@ import { catchError } from 'rxjs/operators'; @Component({ host: { class: 'page' }, imports: [ - CommonModule, GfPremiumIndicatorComponent, IonIcon, MatButtonModule, @@ -78,6 +76,7 @@ export class GfPricingPageComponent implements OnInit { 'Interactive Brokers', 'Mintos', 'Monefit SmartSaver', + 'Revolut', 'Swissquote', 'VIAC', 'Zak' diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index d755cd303..b951baa98 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -103,7 +103,7 @@
    @@ -169,7 +169,7 @@ @if (label) { diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss index 64d4bba80..dc9317c81 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.scss +++ b/apps/client/src/app/pages/pricing/pricing-page.scss @@ -18,8 +18,12 @@ border-color 0.5s ease, box-shadow 0.5s ease; - &:hover, &.active { + border-color: currentColor; + box-shadow: 0 0 0 1px currentColor; + } + + &:hover { border-color: rgba(var(--palette-primary-500), 1); box-shadow: 0 0 0 1px rgba(var(--palette-primary-500), 1); } diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index ab2bd2b4a..43eb23eb9 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -14,7 +14,6 @@ import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart'; -import { CommonModule } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectorRef, @@ -40,7 +39,6 @@ import { catchError } from 'rxjs/operators'; @Component({ host: { class: 'page' }, imports: [ - CommonModule, GfActivitiesTableComponent, GfHoldingsTableComponent, GfPortfolioProportionChartComponent, @@ -174,16 +172,16 @@ export class GfPublicPageComponent implements OnInit { this.holdings.push(position); this.positions[symbol] = { - currency: position.currency, - name: position.name, + currency: position.assetProfile.currency, + name: position.assetProfile.name, value: position.allocationInPercentage }; - if (position.assetClass !== AssetClass.LIQUIDITY) { + if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) { // Prepare analysis data by continents, countries, holdings and sectors except for liquidity - if (position.countries.length > 0) { - for (const country of position.countries) { + if (position.assetProfile.countries.length > 0) { + for (const country of position.assetProfile.countries) { const { code, continent, name, weight } = country; if (this.continents[continent]?.value) { @@ -222,8 +220,8 @@ export class GfPublicPageComponent implements OnInit { 0; } - if (position.sectors.length > 0) { - for (const sector of position.sectors) { + if (position.assetProfile.sectors.length > 0) { + for (const sector of position.assetProfile.sectors) { const { name, weight } = sector; if (this.sectors[name]?.value) { @@ -247,7 +245,7 @@ export class GfPublicPageComponent implements OnInit { } this.symbols[prettifySymbol(symbol)] = { - name: position.name, + name: position.assetProfile.name, symbol: prettifySymbol(symbol), value: isNumber(position.valueInBaseCurrency) ? position.valueInBaseCurrency diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 44696cd7c..57bc1e95f 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -202,7 +202,7 @@

    } -
    +
    diff --git a/apps/client/src/app/pages/resources/glossary/resources-glossary.component.html b/apps/client/src/app/pages/resources/glossary/resources-glossary.component.html index fa74cd084..5f4769721 100644 --- a/apps/client/src/app/pages/resources/glossary/resources-glossary.component.html +++ b/apps/client/src/app/pages/resources/glossary/resources-glossary.component.html @@ -132,7 +132,7 @@
    -
    +

    Stealth Wealth

    diff --git a/apps/client/src/app/pages/resources/guides/resources-guides.component.html b/apps/client/src/app/pages/resources/guides/resources-guides.component.html index 54b3d1f3e..24a7532b9 100644 --- a/apps/client/src/app/pages/resources/guides/resources-guides.component.html +++ b/apps/client/src/app/pages/resources/guides/resources-guides.component.html @@ -19,7 +19,7 @@
    -
    +

    How do I get my finances in order?

    diff --git a/apps/client/src/app/pages/resources/markets/resources-markets.component.html b/apps/client/src/app/pages/resources/markets/resources-markets.component.html index ce780aedf..7437da1b1 100644 --- a/apps/client/src/app/pages/resources/markets/resources-markets.component.html +++ b/apps/client/src/app/pages/resources/markets/resources-markets.component.html @@ -30,8 +30,8 @@
    -
    -
    +
    +

    Inflation Chart

    Inflation Chart helps you find the intrinsic value of stock diff --git a/apps/client/src/app/pages/resources/overview/resources-overview.component.html b/apps/client/src/app/pages/resources/overview/resources-overview.component.html index 86a334c79..8d1665373 100644 --- a/apps/client/src/app/pages/resources/overview/resources-overview.component.html +++ b/apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4,7 +4,7 @@

    Resources

    @for (item of overviewItems; track item) { -
    +

    {{ item.title }}

    {{ item.description }}

    -
    +

    Discover Open Source Alternatives for Personal Finance Tools @@ -22,7 +22,7 @@ personalFinanceTool of personalFinanceTools; track personalFinanceTool ) { - +
    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 016f92fa8..52980be85 100644 --- a/apps/client/src/app/pages/resources/resources-page.component.ts +++ b/apps/client/src/app/pages/resources/resources-page.component.ts @@ -1,10 +1,10 @@ -import { TabConfiguration } from '@ghostfolio/common/interfaces'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; +import { + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; -import { Component, OnInit } from '@angular/core'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; +import { Component } from '@angular/core'; import { addIcons } from 'ionicons'; import { bookOutline, @@ -12,17 +12,15 @@ import { newspaperOutline, readerOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-resources-page', styleUrls: ['./resources-page.scss'], templateUrl: './resources-page.html' }) -export class ResourcesPageComponent implements OnInit { - public deviceType: string; +export class ResourcesPageComponent { public tabs: TabConfiguration[] = [ { iconName: 'reader-outline', @@ -46,11 +44,7 @@ export class ResourcesPageComponent implements OnInit { } ]; - public constructor(private deviceDetectorService: DeviceDetectorService) { + public constructor() { addIcons({ bookOutline, libraryOutline, newspaperOutline, readerOutline }); } - - public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - } } diff --git a/apps/client/src/app/pages/resources/resources-page.html b/apps/client/src/app/pages/resources/resources-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/resources/resources-page.html +++ b/apps/client/src/app/pages/resources/resources-page.html @@ -1,30 +1 @@ - - - - - + 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 7d5af5423..ec7547b3c 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 @@ -1,39 +1,30 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; - import { - ChangeDetectorRef, - Component, - CUSTOM_ELEMENTS_SCHEMA, - DestroyRef, - OnInit -} from '@angular/core'; + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; + +import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-user-account-page', styleUrls: ['./user-account-page.scss'], templateUrl: './user-account-page.html' }) -export class GfUserAccountPageComponent implements OnInit { - public deviceType: string; +export class GfUserAccountPageComponent { public tabs: TabConfiguration[] = []; public user: User; public constructor( private changeDetectorRef: ChangeDetectorRef, private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, private userService: UserService ) { this.userService.stateChanged @@ -68,8 +59,4 @@ export class GfUserAccountPageComponent implements OnInit { addIcons({ diamondOutline, keyOutline, settingsOutline }); } - - public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - } } diff --git a/apps/client/src/app/pages/user-account/user-account-page.html b/apps/client/src/app/pages/user-account/user-account-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/user-account/user-account-page.html +++ b/apps/client/src/app/pages/user-account/user-account-page.html @@ -1,30 +1 @@ - - - - - + 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 b3c14b4e0..4a897093c 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -1,37 +1,30 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; +import { User } from '@ghostfolio/common/interfaces'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; - import { - ChangeDetectorRef, - Component, - DestroyRef, - OnInit -} from '@angular/core'; + GfPageTabsComponent, + TabConfiguration +} from '@ghostfolio/ui/page-tabs'; + +import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatTabsModule } from '@angular/material/tabs'; -import { RouterModule } from '@angular/router'; -import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { albumsOutline, analyticsOutline } from 'ionicons/icons'; -import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ - host: { class: 'page has-tabs' }, - imports: [IonIcon, MatTabsModule, RouterModule], + host: { class: 'page' }, + imports: [GfPageTabsComponent], selector: 'gf-zen-page', styleUrls: ['./zen-page.scss'], templateUrl: './zen-page.html' }) -export class GfZenPageComponent implements OnInit { - public deviceType: string; +export class GfZenPageComponent { public tabs: TabConfiguration[] = []; public user: User; public constructor( private changeDetectorRef: ChangeDetectorRef, private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, private userService: UserService ) { this.userService.stateChanged @@ -58,8 +51,4 @@ export class GfZenPageComponent implements OnInit { addIcons({ albumsOutline, analyticsOutline }); } - - public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - } } diff --git a/apps/client/src/app/pages/zen/zen-page.html b/apps/client/src/app/pages/zen/zen-page.html index bd1338774..5d1bdce9b 100644 --- a/apps/client/src/app/pages/zen/zen-page.html +++ b/apps/client/src/app/pages/zen/zen-page.html @@ -1,30 +1 @@ - - - - - + diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 5a80bb0df..10fa62f11 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -10,7 +10,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -34,11 +34,11 @@ Iniciar sessió apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -455,7 +455,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -511,7 +511,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -531,15 +531,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -651,7 +651,7 @@ Realment vol suprimir aquest compte? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -715,11 +715,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -822,12 +822,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Data apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -899,7 +907,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -915,7 +923,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -939,7 +947,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -1039,7 +1047,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -1051,7 +1059,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -1063,11 +1071,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1087,7 +1095,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -1107,7 +1115,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1183,7 +1191,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -1227,7 +1235,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1235,7 +1243,7 @@ Està segur qeu vol eliminar aquest cupó? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -1243,7 +1251,7 @@ Està segur que vol eliminar aquest missatge del sistema? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -1251,7 +1259,7 @@ Està segur que vol depurar el cache? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -1259,7 +1267,7 @@ Si us plau, afegeixi el seu missatge del sistema: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1275,7 +1283,7 @@ per Usuari apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1314,6 +1322,14 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + System Message Missatge del Sistema @@ -1515,7 +1531,7 @@ Implicació per Dia apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1527,7 +1543,7 @@ Última Solicitut apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1535,7 +1551,7 @@ Actuar com un altre Usuari apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1543,7 +1559,7 @@ Eliminar Usuari apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1587,7 +1603,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1667,7 +1683,7 @@ Sobre Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1679,7 +1695,7 @@ Oooh! El testimoni de seguretat és incorrecte. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1695,7 +1711,7 @@ Preu Mínim apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1703,7 +1719,7 @@ Preu Màxim apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1711,11 +1727,11 @@ Quantitat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1731,7 +1747,7 @@ Rendiment del Dividend apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -1739,7 +1755,7 @@ Comissions apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1751,7 +1767,7 @@ Indonèsia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -1759,7 +1775,7 @@ Activitat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -1775,7 +1791,7 @@ en Actiiu apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -1783,7 +1799,7 @@ Finalitzat apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -1823,7 +1839,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1839,7 +1855,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1883,7 +1899,7 @@ Configura els teus comptes apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -1891,7 +1907,7 @@ Obtingueu una visió financera completa afegint els vostres comptes bancaris i de corredoria. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -1899,7 +1915,7 @@ Captura les teves activitats apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -1907,7 +1923,7 @@ Registra les teves activitats d’inversió per mantenir la teva cartera actualitzada. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -1915,7 +1931,7 @@ Superviseu i analitzeu la vostra cartera apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -1923,7 +1939,7 @@ Segueix el teu progrés en temps real amb anàlisis i informació exhaustiva. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -1931,7 +1947,7 @@ Configurar comptes apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1947,7 +1963,7 @@ Afegeix activitat apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1959,7 +1975,7 @@ Import total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1975,7 +1991,7 @@ Taxa d’estalvi apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2079,7 +2095,7 @@ Les dades del mercat s’han retardat apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -2143,7 +2159,7 @@ Actius apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -2151,7 +2167,7 @@ Poder adquisitiu apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -2159,7 +2175,7 @@ Exclòs de l’anàlisi apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2167,7 +2183,7 @@ Passius apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -2179,7 +2195,7 @@ Valor net apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -2187,7 +2203,7 @@ Rendiment anualitzat apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -2195,7 +2211,7 @@ Definiu l’import del vostre fons d’emergència. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -2347,7 +2363,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -2359,7 +2375,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -2371,7 +2387,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2383,7 +2399,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2403,7 +2419,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2419,7 +2435,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -2558,6 +2574,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Do you really want to remove this sign in method? De debò vols eliminar aquest mètode d’inici de sessió? @@ -2771,7 +2795,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2791,7 +2815,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2807,7 +2831,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2831,7 +2855,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2931,7 +2955,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2939,11 +2963,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3027,7 +3051,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -3047,7 +3071,7 @@ Dades de mercat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3099,7 +3123,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -3107,11 +3131,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -3295,7 +3319,7 @@ General apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -3303,7 +3327,7 @@ Núvol apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -3315,7 +3339,7 @@ Autoallotjament apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -3444,7 +3468,7 @@ Comença apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -3524,7 +3548,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -3540,7 +3564,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -4040,15 +4064,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -4132,7 +4156,7 @@ Actualitzar el saldo d’efectiu apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -4140,7 +4164,7 @@ Preu unitari apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4152,7 +4176,7 @@ Activitats d’importació apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4168,7 +4192,7 @@ Importar dividends apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4184,7 +4208,7 @@ S’estan important dades... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -4192,7 +4216,7 @@ La importació s’ha completat apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -4208,7 +4232,7 @@ S’estan validant les dades... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -4392,7 +4416,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -4468,7 +4492,7 @@ Per ETF Holding apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -4476,7 +4500,7 @@ Aproximació basada en les participacions principals de cada ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -4508,11 +4532,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -4520,7 +4544,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -4528,7 +4552,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -4536,7 +4560,7 @@ Inversió apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -4584,7 +4608,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -4704,11 +4728,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -4940,7 +4964,7 @@ Inscripció apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -5033,7 +5057,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5045,7 +5069,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5293,7 +5317,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -5341,7 +5365,7 @@ El meu Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -5457,7 +5481,7 @@ Setmana fins avui libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5469,7 +5493,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5477,7 +5501,7 @@ Mes fins a la data libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -5489,7 +5513,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -5497,7 +5521,7 @@ Any fins a la data libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -5517,7 +5541,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -5529,7 +5553,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -5629,7 +5653,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -5697,7 +5721,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -5705,7 +5729,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -5749,7 +5773,7 @@ Compte apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5789,11 +5813,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -5821,11 +5845,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -5864,14 +5888,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Canvia fàcilment a Ghostfolio Open Source o Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund Fons d’emergència @@ -5885,7 +5901,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -5893,7 +5909,7 @@ Subvenció libs/ui/src/lib/i18n.ts - 19 + 18 @@ -5901,7 +5917,7 @@ Risc Alt libs/ui/src/lib/i18n.ts - 20 + 19 @@ -5909,7 +5925,7 @@ Aquesta activitat ja existeix. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -5917,7 +5933,7 @@ Japó libs/ui/src/lib/i18n.ts - 93 + 92 @@ -5925,7 +5941,7 @@ Risc Baix libs/ui/src/lib/i18n.ts - 22 + 21 @@ -5933,7 +5949,7 @@ Mes libs/ui/src/lib/i18n.ts - 23 + 22 @@ -5941,7 +5957,7 @@ Mesos libs/ui/src/lib/i18n.ts - 24 + 23 @@ -5949,7 +5965,7 @@ Altres libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -5961,7 +5977,7 @@ Predefinit libs/ui/src/lib/i18n.ts - 27 + 26 @@ -5977,7 +5993,7 @@ Provisió de jubilació libs/ui/src/lib/i18n.ts - 28 + 27 @@ -5993,7 +6009,7 @@ Satèl·lit libs/ui/src/lib/i18n.ts - 29 + 28 @@ -6017,11 +6033,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -6029,7 +6045,7 @@ Etiqueta libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -6041,7 +6057,7 @@ Any libs/ui/src/lib/i18n.ts - 32 + 31 @@ -6049,7 +6065,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -6061,7 +6077,7 @@ Anys libs/ui/src/lib/i18n.ts - 33 + 32 @@ -6081,7 +6097,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -6089,7 +6105,7 @@ Comissió apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -6097,7 +6113,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -6105,7 +6121,7 @@ Valuós libs/ui/src/lib/i18n.ts - 43 + 42 @@ -6113,7 +6129,7 @@ Passiu libs/ui/src/lib/i18n.ts - 41 + 40 @@ -6125,7 +6141,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -6133,11 +6149,11 @@ Efectiu apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -6145,7 +6161,7 @@ Mercaderia libs/ui/src/lib/i18n.ts - 47 + 46 @@ -6157,7 +6173,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -6165,7 +6181,7 @@ Ingressos Fixos libs/ui/src/lib/i18n.ts - 49 + 48 @@ -6173,7 +6189,7 @@ Liquiditat libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6181,7 +6197,7 @@ Immobiliari libs/ui/src/lib/i18n.ts - 51 + 50 @@ -6197,7 +6213,7 @@ Bona libs/ui/src/lib/i18n.ts - 54 + 53 @@ -6205,7 +6221,7 @@ Criptomoneda libs/ui/src/lib/i18n.ts - 57 + 56 @@ -6213,7 +6229,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -6221,7 +6237,7 @@ Fons d’inversió libs/ui/src/lib/i18n.ts - 60 + 59 @@ -6229,7 +6245,7 @@ Metall preciós libs/ui/src/lib/i18n.ts - 61 + 60 @@ -6237,7 +6253,7 @@ Capital privat libs/ui/src/lib/i18n.ts - 62 + 61 @@ -6245,7 +6261,7 @@ Acció libs/ui/src/lib/i18n.ts - 63 + 62 @@ -6253,7 +6269,7 @@ Àfrica libs/ui/src/lib/i18n.ts - 70 + 69 @@ -6261,7 +6277,7 @@ Àsia libs/ui/src/lib/i18n.ts - 71 + 70 @@ -6269,7 +6285,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -6277,7 +6293,7 @@ Amèrica del Nord libs/ui/src/lib/i18n.ts - 73 + 72 @@ -6293,7 +6309,7 @@ Oceania libs/ui/src/lib/i18n.ts - 74 + 73 @@ -6301,7 +6317,7 @@ Amèrica del Sud libs/ui/src/lib/i18n.ts - 75 + 74 @@ -6309,7 +6325,7 @@ Por extrema libs/ui/src/lib/i18n.ts - 107 + 106 @@ -6317,7 +6333,7 @@ Avarícia extrema libs/ui/src/lib/i18n.ts - 108 + 107 @@ -6325,7 +6341,7 @@ Neutral libs/ui/src/lib/i18n.ts - 111 + 110 @@ -6373,7 +6389,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -6381,7 +6397,7 @@ Mostra més libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Australia libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Austria libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ Belgium libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bulgaria libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Canada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ Czech Republic libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finland libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ France libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Germany libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ India libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ Italy libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Netherlands libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ New Zealand libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Poland libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Romania libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ South Africa libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Thailand libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ United States libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Inactive apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Yes libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ Get access to 80’000+ tickers from over 50 exchanges libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Ukraine libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Please enter your Ghostfolio API key: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ API Requests Today apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ British Virgin Islands libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Generate Security Token apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ United Kingdom libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Do you really want to delete this item? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Log out apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo user account has been synced. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Alternative Investment libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Collectible libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 38c7db874..bfff88889 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -138,7 +138,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -182,15 +182,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -302,7 +302,7 @@ Möchtest du dieses Konto wirklich löschen? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -330,11 +330,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -437,12 +437,20 @@ 2 + + Find an account... + Finde ein Konto... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Datum apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -486,7 +494,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -502,7 +510,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -510,7 +518,7 @@ Möchtest du diesen Gutscheincode wirklich löschen? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -518,7 +526,7 @@ Möchtest du den Cache wirklich leeren? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -526,7 +534,7 @@ Bitte gebe deine Systemmeldung ein: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -534,7 +542,7 @@ pro Benutzer apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -670,7 +678,7 @@ Engagement pro Tag apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -682,7 +690,7 @@ Letzte Abfrage apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -698,7 +706,7 @@ Über Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -710,7 +718,7 @@ Registrieren apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -742,11 +750,11 @@ Einloggen apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -766,7 +774,7 @@ Ups! Falsches Sicherheits-Token. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -938,7 +946,7 @@ Kaufkraft apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -946,7 +954,7 @@ Gesamtvermögen apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -954,7 +962,7 @@ Performance pro Jahr apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -962,7 +970,7 @@ Bitte setze den Betrag deines Notfallfonds. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -978,7 +986,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -998,7 +1006,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1062,7 +1070,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1074,7 +1082,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1086,7 +1094,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1098,7 +1106,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1118,7 +1126,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1134,7 +1142,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1150,7 +1158,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1182,7 +1190,7 @@ Mein Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1370,7 +1378,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1378,11 +1386,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1438,7 +1446,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -1498,7 +1506,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1662,7 +1670,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -1690,7 +1698,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1698,11 +1706,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -1726,7 +1734,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -1742,7 +1750,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -1946,7 +1954,7 @@ Aktivität hinzufügen apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1962,7 +1970,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -1982,7 +1990,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1990,11 +1998,11 @@ Anzahl apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2010,7 +2018,7 @@ Stückpreis apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2030,7 +2038,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -2058,15 +2066,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2090,7 +2098,7 @@ Daten importieren... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2098,7 +2106,7 @@ Der Import wurde abgeschlossen apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2122,11 +2130,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2154,7 +2162,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -2194,7 +2202,7 @@ Registrierung apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2234,7 +2242,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2282,7 +2290,7 @@ Aktivitäten importieren apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2410,7 +2418,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2422,7 +2430,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2430,7 +2438,7 @@ Minimum Preis apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -2438,7 +2446,7 @@ Maximum Preis apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -2450,7 +2458,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -2462,11 +2470,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2482,7 +2490,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2570,7 +2578,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2578,7 +2586,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2618,7 +2626,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -2634,7 +2642,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -2642,7 +2650,7 @@ Filtern nach... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -2718,7 +2726,7 @@ Von der Analyse ausgenommen apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2762,7 +2770,7 @@ Gesamtbetrag apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -2786,7 +2794,7 @@ Sparrate apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2794,7 +2802,7 @@ Konto apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2826,11 +2834,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2862,11 +2870,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -2874,7 +2882,7 @@ Tag libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -2886,11 +2894,11 @@ Bargeld apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -2898,7 +2906,7 @@ Rohstoff libs/ui/src/lib/i18n.ts - 47 + 46 @@ -2910,7 +2918,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -2918,7 +2926,7 @@ Feste Einkünfte libs/ui/src/lib/i18n.ts - 49 + 48 @@ -2926,7 +2934,7 @@ Immobilien libs/ui/src/lib/i18n.ts - 51 + 50 @@ -2942,7 +2950,7 @@ Anleihe libs/ui/src/lib/i18n.ts - 54 + 53 @@ -2950,7 +2958,7 @@ Kryptowährung libs/ui/src/lib/i18n.ts - 57 + 56 @@ -2958,7 +2966,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -2966,7 +2974,7 @@ Investmentfonds libs/ui/src/lib/i18n.ts - 60 + 59 @@ -2974,7 +2982,7 @@ Edelmetall libs/ui/src/lib/i18n.ts - 61 + 60 @@ -2982,7 +2990,7 @@ Privates Beteiligungskapital libs/ui/src/lib/i18n.ts - 62 + 61 @@ -2990,7 +2998,7 @@ Aktie libs/ui/src/lib/i18n.ts - 63 + 62 @@ -3006,7 +3014,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -3014,7 +3022,7 @@ Andere libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3046,7 +3054,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3054,7 +3062,7 @@ Nordamerika libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3062,7 +3070,7 @@ Afrika libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3070,7 +3078,7 @@ Asien libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3078,7 +3086,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3094,7 +3102,7 @@ Ozeanien libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3102,7 +3110,7 @@ Südamerika libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3174,11 +3182,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3186,7 +3194,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3194,7 +3202,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3214,11 +3222,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3238,7 +3246,7 @@ Daten validieren... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3254,7 +3262,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3262,7 +3270,7 @@ Marktdaten apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3326,7 +3334,7 @@ Dividenden importieren apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3362,7 +3370,7 @@ Zuwendung libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3370,7 +3378,7 @@ Höheres Risiko libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3378,7 +3386,7 @@ Geringeres Risiko libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3394,7 +3402,7 @@ Altersvorsorge libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3410,7 +3418,7 @@ Satellit libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3682,7 +3690,7 @@ Gebühren apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3761,20 +3769,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Einfacher Wechsel zu Ghostfolio Open Source oder Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Loan Darlehen libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3850,7 +3850,7 @@ Benutzer verwenden apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3858,7 +3858,7 @@ Benutzer löschen apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3966,7 +3966,7 @@ Cash-Bestand aktualisieren apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -4022,7 +4022,7 @@ Diese Aktivität existiert bereits. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4102,7 +4102,7 @@ Monate libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4110,7 +4110,7 @@ Jahre libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4118,7 +4118,7 @@ Monat libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4126,7 +4126,7 @@ Jahr libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4134,7 +4134,7 @@ Details anzeigen apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4146,7 +4146,7 @@ Verbindlichkeiten apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4286,7 +4286,7 @@ Verbindlichkeit libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4530,7 +4530,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4538,7 +4538,7 @@ Wertsache libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4562,7 +4562,7 @@ Anlagevermögen apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4570,7 +4570,7 @@ Filtervorlage libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4594,7 +4594,7 @@ Japan libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4618,7 +4618,7 @@ Konten einrichten apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4626,7 +4626,7 @@ Verschaffe dir einen umfassenden Überblick, indem du deine Bank- und Wertpapierkonten hinzufügst. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4634,7 +4634,7 @@ Aktivitäten erfassen apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4642,7 +4642,7 @@ Erfasse deine Investitionsaktivitäten, um dein Portfolio auf dem neuesten Stand zu halten. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4650,7 +4650,7 @@ Portfolio überwachen und analysieren apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4658,7 +4658,7 @@ Verfolge die Entwicklung in Echtzeit mit umfassenden Analysen und Einblicken. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4674,7 +4674,7 @@ Konten einrichten apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5404,7 +5404,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5416,7 +5416,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5508,7 +5508,7 @@ Gebühr apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5516,7 +5516,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5740,7 +5740,7 @@ Extreme Angst libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5748,7 +5748,7 @@ Extreme Gier libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5756,7 +5756,7 @@ Neutral libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5772,7 +5772,7 @@ Möchtest du diese Systemmeldung wirklich löschen? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5888,7 +5888,7 @@ Argentinien libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5912,7 +5912,7 @@ Die Marktdaten sind verzögert für apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5920,7 +5920,7 @@ Investition apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5952,7 +5952,7 @@ Position abschliessen apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5992,7 +5992,7 @@ Seit Wochenbeginn libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6004,7 +6004,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6012,7 +6012,7 @@ Seit Monatsbeginn libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6024,7 +6024,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6032,7 +6032,7 @@ Seit Jahresbeginn libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6080,7 +6080,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6092,7 +6092,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6115,12 +6115,20 @@ 76 + + Find a holding... + Finde eine Position... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Allgemein apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6128,7 +6136,7 @@ Cloud apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6140,7 +6148,7 @@ Self-Hosting apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6181,7 +6189,7 @@ Aktiv apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6189,7 +6197,7 @@ Abgeschlossen apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6197,7 +6205,7 @@ Indonesien libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6205,7 +6213,7 @@ Aktivität apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6213,7 +6221,7 @@ Dividendenrendite apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6245,7 +6253,7 @@ Liquidität libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6288,6 +6296,14 @@ 205 + + Jump to a page... + Springe zu einer Seite... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Gefahrenzone @@ -6309,7 +6325,7 @@ Nach ETF Position apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6317,7 +6333,7 @@ Annäherung auf Basis der grössten Positionen pro ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6349,7 +6365,7 @@ Mehr anzeigen libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6585,7 +6601,7 @@ Australien libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6593,7 +6609,7 @@ Österreich libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6601,7 +6617,7 @@ Belgien libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6609,7 +6625,7 @@ Bulgarien libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6625,7 +6641,7 @@ Kanada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6633,7 +6649,7 @@ Tschechien libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6641,7 +6657,7 @@ Finnland libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6649,7 +6665,7 @@ Frankreich libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6657,7 +6673,7 @@ Deutschland libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6665,7 +6681,7 @@ Indien libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6673,7 +6689,7 @@ Italien libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6681,7 +6697,7 @@ Niederlande libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6689,7 +6705,7 @@ Neuseeland libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6697,7 +6713,7 @@ Polen libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6705,7 +6721,7 @@ Rumänien libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6713,7 +6729,7 @@ Südafrika libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6721,7 +6737,7 @@ Thailand libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6729,7 +6745,7 @@ USA libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6777,7 +6793,7 @@ Inaktiv apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6821,7 +6837,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6861,7 +6877,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6877,7 +6893,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6897,7 +6913,7 @@ Ja libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6945,7 +6961,7 @@ Schwellenwert (Minimum) apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6953,7 +6969,7 @@ Schwellenwert (Maximum) apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6985,7 +7001,7 @@ wurde in die Zwischenablage kopiert apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7165,7 +7181,7 @@ Erhalte Zugang zu 80’000+ Tickern von über 50 Handelsplätzen libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7173,7 +7189,7 @@ Ukraine libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7193,7 +7209,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7209,7 +7225,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7295,7 +7311,7 @@ Bitte gib den API-Schlüssel ein: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7303,7 +7319,7 @@ Heutige API Anfragen apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7399,11 +7415,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7415,7 +7431,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7571,7 +7587,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7583,7 +7599,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7591,11 +7607,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7635,7 +7651,7 @@ Armenien libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7643,7 +7659,7 @@ Britische Jungferninseln libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7651,7 +7667,7 @@ Singapur libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7714,20 +7730,12 @@ 240 - - Find account, holding or page... - Konto, Position oder Seite finden... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Sicherheits-Token generieren apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7735,7 +7743,7 @@ Vereinigtes Königreich libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ jemand apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Möchtest du diesen Eintrag wirklich löschen? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Ausloggen apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo Benutzerkonto wurde synchronisiert. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Alternative Investition libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Sammlerobjekt libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 62befb2ae..c99924de8 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -139,7 +139,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -183,15 +183,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -303,7 +303,7 @@ ¿Seguro que quieres eliminar esta cuenta? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -331,11 +331,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -438,12 +438,20 @@ 2 + + Find an account... + Buscar una cuenta... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Fecha apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -487,7 +495,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -503,7 +511,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -511,7 +519,7 @@ ¿Seguro que quieres eliminar este cupón? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -519,7 +527,7 @@ ¿Seguro que quieres limpiar la caché? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -527,7 +535,7 @@ Por favor, establece tu mensaje del sistema: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -535,7 +543,7 @@ por usuario apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -655,7 +663,7 @@ Interacción diaria apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -667,7 +675,7 @@ Última petición apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -683,7 +691,7 @@ Sobre Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -695,7 +703,7 @@ Empezar apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -727,11 +735,11 @@ Iniciar sesión apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -751,7 +759,7 @@ ¡Vaya! Token de seguridad incorrecto. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -923,7 +931,7 @@ Poder adquisitivo apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -931,7 +939,7 @@ Patrimonio neto apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -939,7 +947,7 @@ Rendimiento anualizado apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -947,7 +955,7 @@ Por favor, ingresa la cantidad de tu fondo de emergencia. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -963,7 +971,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -983,7 +991,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1047,7 +1055,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1059,7 +1067,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1071,7 +1079,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1083,7 +1091,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1103,7 +1111,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1119,7 +1127,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1135,7 +1143,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1167,7 +1175,7 @@ Mi Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1355,7 +1363,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1363,11 +1371,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1423,7 +1431,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -1483,7 +1491,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1612,7 +1620,7 @@ Duration - Duration + Duración apps/client/src/app/components/admin-overview/admin-overview.html 172 @@ -1647,7 +1655,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -1675,7 +1683,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1683,11 +1691,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -1711,7 +1719,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -1727,7 +1735,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -1931,7 +1939,7 @@ Añadir operación apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1947,7 +1955,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -1967,7 +1975,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1975,11 +1983,11 @@ Cantidad apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1995,7 +2003,7 @@ Precio unitario apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2015,7 +2023,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -2043,15 +2051,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2075,7 +2083,7 @@ Importando datos... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2083,7 +2091,7 @@ La importación se ha completado apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2107,11 +2115,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2139,7 +2147,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -2179,7 +2187,7 @@ Registro apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2219,7 +2227,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2267,7 +2275,7 @@ Importar operaciones apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2391,7 +2399,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2407,7 +2415,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2419,7 +2427,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2447,7 +2455,7 @@ Precio máximo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -2483,7 +2491,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -2495,11 +2503,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2511,7 +2519,7 @@ Precio mínimo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -2539,7 +2547,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2547,7 +2555,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2603,7 +2611,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -2619,7 +2627,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -2627,7 +2635,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -2703,7 +2711,7 @@ Excluido del análisis apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2747,7 +2755,7 @@ Importe total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -2760,7 +2768,7 @@ Code - Code + Código apps/client/src/app/components/admin-overview/admin-overview.html 159 @@ -2771,7 +2779,7 @@ Tasa de ahorro apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2779,7 +2787,7 @@ Cuenta apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2811,11 +2819,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2847,11 +2855,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -2859,7 +2867,7 @@ Etiqueta libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -2871,11 +2879,11 @@ Efectivo apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -2883,7 +2891,7 @@ Materia prima libs/ui/src/lib/i18n.ts - 47 + 46 @@ -2895,7 +2903,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -2903,7 +2911,7 @@ Renta fija libs/ui/src/lib/i18n.ts - 49 + 48 @@ -2911,7 +2919,7 @@ Propiedad inmobiliaria libs/ui/src/lib/i18n.ts - 51 + 50 @@ -2927,7 +2935,7 @@ Bono libs/ui/src/lib/i18n.ts - 54 + 53 @@ -2935,7 +2943,7 @@ Criptomoneda libs/ui/src/lib/i18n.ts - 57 + 56 @@ -2943,7 +2951,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -2951,7 +2959,7 @@ Fondo de inversión libs/ui/src/lib/i18n.ts - 60 + 59 @@ -2959,7 +2967,7 @@ Metal precioso libs/ui/src/lib/i18n.ts - 61 + 60 @@ -2967,7 +2975,7 @@ Capital riesgo libs/ui/src/lib/i18n.ts - 62 + 61 @@ -2975,7 +2983,7 @@ Acción libs/ui/src/lib/i18n.ts - 63 + 62 @@ -2991,7 +2999,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -2999,7 +3007,7 @@ Otros libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3031,7 +3039,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3039,7 +3047,7 @@ América del Norte libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3047,7 +3055,7 @@ África libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3055,7 +3063,7 @@ Asia libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3063,7 +3071,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3079,7 +3087,7 @@ Oceanía libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3087,7 +3095,7 @@ América del Sur libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3151,11 +3159,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3163,7 +3171,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3171,7 +3179,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3199,11 +3207,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3223,7 +3231,7 @@ Validando datos... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3239,7 +3247,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3247,7 +3255,7 @@ Datos del mercado apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3311,7 +3319,7 @@ Importar dividendos apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3347,7 +3355,7 @@ Conceder libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3355,7 +3363,7 @@ Mayor riesgo libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3363,7 +3371,7 @@ Menor riesgo libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3379,7 +3387,7 @@ Provisión de jubilación libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3395,7 +3403,7 @@ Satélite libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3667,7 +3675,7 @@ Comisiones apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3746,20 +3754,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Cambia a Ghostfolio Open Source o Ghostfolio Basic fácilmente - - libs/ui/src/lib/i18n.ts - 14 - - Loan - Loan + Préstamo libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3827,7 +3827,7 @@ Suplantar usuario apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3835,7 +3835,7 @@ Eliminar usuario apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3943,7 +3943,7 @@ Actualizar saldo en efectivo apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3999,7 +3999,7 @@ Esta operación ya existe. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4079,7 +4079,7 @@ Meses libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4087,7 +4087,7 @@ Años libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4095,7 +4095,7 @@ Mes libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4103,7 +4103,7 @@ Año libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4111,7 +4111,7 @@ Ver detalles apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4123,7 +4123,7 @@ Pasivos apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4263,7 +4263,7 @@ Pasivo libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4507,7 +4507,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4515,7 +4515,7 @@ Activo de valor libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4539,7 +4539,7 @@ Activos apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4547,7 +4547,7 @@ Preestablecido libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4571,7 +4571,7 @@ Japón libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4595,7 +4595,7 @@ Configura tus cuentas apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4603,7 +4603,7 @@ Obtén una visión financiera completa agregando tus cuentas bancarias y de corretaje. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4611,7 +4611,7 @@ Captura tus operaciones apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4619,7 +4619,7 @@ Registra tus operaciones de inversión para mantener tu cartera actualizada. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4627,7 +4627,7 @@ Monitorea y analiza tu cartera apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4635,7 +4635,7 @@ Sigue tu progreso en tiempo real con análisis e información detallada. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4651,7 +4651,7 @@ Configura tus cuentas apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5012,7 +5012,7 @@ saying no to spreadsheets in - diciendo no a las hojas de cálculo en + en contra de las las hojas de cálculo en apps/client/src/app/pages/landing/landing-page.html 209 @@ -5381,7 +5381,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5393,7 +5393,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5485,7 +5485,7 @@ Comisión apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5493,7 +5493,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5717,7 +5717,7 @@ Miedo extremo libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5725,7 +5725,7 @@ Codicia extrema libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5733,7 +5733,7 @@ Neutral libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5749,7 +5749,7 @@ ¿Seguro que quieres eliminar este mensaje del sistema? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5865,7 +5865,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5889,7 +5889,7 @@ Los datos del mercado tienen un retraso de apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5897,7 +5897,7 @@ Inversión apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5929,7 +5929,7 @@ Cerrar posición apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5969,7 +5969,7 @@ Semana hasta la fecha libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5981,7 +5981,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5989,7 +5989,7 @@ Mes hasta la fecha libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6001,7 +6001,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6009,7 +6009,7 @@ Año hasta la fecha libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6057,7 +6057,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6069,7 +6069,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6092,12 +6092,20 @@ 76 + + Find a holding... + Buscar una posición... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General General apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6105,7 +6113,7 @@ Nube apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6117,7 +6125,7 @@ Autoalojamiento apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6158,7 +6166,7 @@ Activo apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6166,7 +6174,7 @@ Cerrado apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6174,7 +6182,7 @@ Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6182,7 +6190,7 @@ Operación apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6190,7 +6198,7 @@ Rendimiento por dividendo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6222,7 +6230,7 @@ Liquidez libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6265,6 +6273,14 @@ 205 + + Jump to a page... + Saltar a una página... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Zona peligrosa @@ -6286,7 +6302,7 @@ Por tenencia de ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6294,7 +6310,7 @@ Aproximación basada en las principales posiciones de cada ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6326,7 +6342,7 @@ Mostrar más libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6562,7 +6578,7 @@ Australia libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6570,7 +6586,7 @@ Austria libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6578,7 +6594,7 @@ Bélgica libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6586,7 +6602,7 @@ Bulgaria libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6602,7 +6618,7 @@ Canadá libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6610,7 +6626,7 @@ República Checa libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6618,7 +6634,7 @@ Finlandia libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6626,7 +6642,7 @@ Francia libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6634,7 +6650,7 @@ Alemania libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6642,7 +6658,7 @@ India libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6650,7 +6666,7 @@ Italia libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6658,7 +6674,7 @@ Países Bajos libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6666,7 +6682,7 @@ Nueva Zelanda libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6674,7 +6690,7 @@ Polonia libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6682,7 +6698,7 @@ Rumanía libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6690,7 +6706,7 @@ Sudáfrica libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6698,7 +6714,7 @@ Tailandia libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6706,7 +6722,7 @@ Estados Unidos libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6754,7 +6770,7 @@ Inactiva apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6798,7 +6814,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6838,7 +6854,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6854,7 +6870,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6874,7 +6890,7 @@ libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6922,7 +6938,7 @@ Umbral mínimo apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6930,7 +6946,7 @@ Umbral máximo apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6959,10 +6975,10 @@ has been copied to the clipboard - has been copied to the clipboard + ha sido copiado al portapapeles apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7142,7 +7158,7 @@ Accede a más de 80.000 tickers de más de 50 bolsas libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7150,7 +7166,7 @@ Ucrania libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7170,7 +7186,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7186,7 +7202,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7272,7 +7288,7 @@ Ingresa tu clave API de Ghostfolio: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7280,7 +7296,7 @@ Solicitudes de API de hoy apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7376,11 +7392,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7392,7 +7408,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7548,7 +7564,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7560,7 +7576,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7568,11 +7584,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7612,7 +7628,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7620,7 +7636,7 @@ Islas Vírgenes Británicas libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7628,7 +7644,7 @@ Singapur libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7691,20 +7707,12 @@ 240 - - Find account, holding or page... - Busca una cuenta, posición o página... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Generar token de seguridad apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7712,7 +7720,7 @@ Reino Unido libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7833,7 +7841,7 @@ alguien apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7865,7 +7873,7 @@ ¿Seguro que quieres eliminar este elemento? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7873,7 +7881,7 @@ Cerrar sesión apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7906,7 +7914,7 @@ La cuenta de usuario de demostración se ha sincronizado. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8333,7 +8341,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8349,7 +8357,7 @@ Inversión alternativa libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8357,7 +8365,7 @@ Coleccionable libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index ed053be45..11d7d0577 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -146,7 +146,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -202,7 +202,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -238,15 +238,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -358,7 +358,7 @@ Voulez-vous vraiment supprimer ce compte ? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -378,11 +378,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -493,12 +493,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Date apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -534,7 +542,7 @@ Filtrer par... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -550,7 +558,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -574,7 +582,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -622,7 +630,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -634,11 +642,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -658,7 +666,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -678,7 +686,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -702,7 +710,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -710,7 +718,7 @@ Voulez-vous vraiment supprimer ce code promotionnel ? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -718,7 +726,7 @@ Voulez-vous vraiment vider le cache ? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -726,7 +734,7 @@ Veuillez définir votre message système : apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -734,7 +742,7 @@ par Utilisateur apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -866,7 +874,7 @@ Engagement par Jour apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -878,7 +886,7 @@ Dernière Requête apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -914,7 +922,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -950,7 +958,7 @@ À propos de Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -962,11 +970,11 @@ Se connecter apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -986,7 +994,7 @@ Oups! Jeton de Sécurité Incorrect. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1018,7 +1026,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1034,7 +1042,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1054,7 +1062,7 @@ Montant Total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1070,7 +1078,7 @@ Taux d’Épargne apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1214,7 +1222,7 @@ Pouvoir d’Achat apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1222,7 +1230,7 @@ Exclus de l’Analyse apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1230,7 +1238,7 @@ Fortune apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1238,7 +1246,7 @@ Performance annualisée apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1246,7 +1254,7 @@ Veuillez entrer le montant de votre fonds d’urgence : apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1254,7 +1262,7 @@ Prix Minimum apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1262,7 +1270,7 @@ Prix Maximum apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1270,11 +1278,11 @@ Quantité apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1302,7 +1310,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1314,7 +1322,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1326,7 +1334,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1338,7 +1346,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1358,7 +1366,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1382,7 +1390,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -1394,7 +1402,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -1410,7 +1418,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1426,7 +1434,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1458,7 +1466,7 @@ Mon Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1698,7 +1706,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1706,11 +1714,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1758,7 +1766,7 @@ Données du marché apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -1774,7 +1782,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1950,7 +1958,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -2014,7 +2022,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -2030,7 +2038,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -2074,15 +2082,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2122,7 +2130,7 @@ Ajouter Activité apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -2138,7 +2146,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -2158,7 +2166,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -2166,7 +2174,7 @@ Prix Unitaire apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2178,7 +2186,7 @@ Import des données... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2186,7 +2194,7 @@ L’import est terminé apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2202,7 +2210,7 @@ Validation des données... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -2238,7 +2246,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -2342,7 +2350,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2418,11 +2426,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -2430,7 +2438,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -2438,7 +2446,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -2534,11 +2542,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2590,7 +2598,7 @@ Démarrer apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -2622,7 +2630,7 @@ Enregistrement apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2682,7 +2690,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2734,7 +2742,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2742,11 +2750,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2770,7 +2778,7 @@ Importer Activités apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2878,7 +2886,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2886,7 +2894,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2930,7 +2938,7 @@ Compte apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2962,11 +2970,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2994,11 +3002,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3018,7 +3026,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -3026,7 +3034,7 @@ Autre libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3054,11 +3062,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -3066,7 +3074,7 @@ Étiquette libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -3078,11 +3086,11 @@ Cash apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -3090,7 +3098,7 @@ Marchandise libs/ui/src/lib/i18n.ts - 47 + 46 @@ -3102,7 +3110,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -3110,7 +3118,7 @@ Revenu Fixe libs/ui/src/lib/i18n.ts - 49 + 48 @@ -3118,7 +3126,7 @@ Immobilier libs/ui/src/lib/i18n.ts - 51 + 50 @@ -3134,7 +3142,7 @@ Obligation libs/ui/src/lib/i18n.ts - 54 + 53 @@ -3142,7 +3150,7 @@ Cryptomonnaie libs/ui/src/lib/i18n.ts - 57 + 56 @@ -3150,7 +3158,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -3158,7 +3166,7 @@ SICAV libs/ui/src/lib/i18n.ts - 60 + 59 @@ -3166,7 +3174,7 @@ Métal Précieux libs/ui/src/lib/i18n.ts - 61 + 60 @@ -3174,7 +3182,7 @@ Capital Propre libs/ui/src/lib/i18n.ts - 62 + 61 @@ -3182,7 +3190,7 @@ Action libs/ui/src/lib/i18n.ts - 63 + 62 @@ -3190,7 +3198,7 @@ Afrique libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3198,7 +3206,7 @@ Asie libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3206,7 +3214,7 @@ Europe libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3214,7 +3222,7 @@ Amérique du Nord libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3230,7 +3238,7 @@ Océanie libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3238,7 +3246,7 @@ Amérique du Sud libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3274,7 +3282,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3310,7 +3318,7 @@ Importer Dividendes apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3346,7 +3354,7 @@ Donner libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3354,7 +3362,7 @@ Risque élevé libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3362,7 +3370,7 @@ Risque faible libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3378,7 +3386,7 @@ Réserve pour retraite libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3394,7 +3402,7 @@ Satellite libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3666,7 +3674,7 @@ Frais apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3745,20 +3753,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Passez à Ghostfolio Open Source ou Ghostfolio Basic facilement - - libs/ui/src/lib/i18n.ts - 14 - - Loan Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3826,7 +3826,7 @@ Voir en tant que ... apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3834,7 +3834,7 @@ Supprimer l’Utilisateur apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3942,7 +3942,7 @@ Mettre à jour le Solde apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3998,7 +3998,7 @@ Cette activité existe déjà. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4078,7 +4078,7 @@ Mois libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4086,7 +4086,7 @@ Années libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4094,7 +4094,7 @@ Mois libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4102,7 +4102,7 @@ Année libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4110,7 +4110,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4122,7 +4122,7 @@ Dettes apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4262,7 +4262,7 @@ Dette libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4506,7 +4506,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4514,7 +4514,7 @@ Actifs libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4538,7 +4538,7 @@ Actifs apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4546,7 +4546,7 @@ Configuration par défaut libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4570,7 +4570,7 @@ Japon libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4594,7 +4594,7 @@ Configurer votre compte apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4602,7 +4602,7 @@ Obtenir un aperçu de vos comptes financiers en ajoutant vos comptes bancaires et de courtages. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4610,7 +4610,7 @@ Capture vos activitées apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4618,7 +4618,7 @@ Enregistrez vos activités immobilières pour maintenir votre portfolio à jour. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4626,7 +4626,7 @@ Monitorer et analyser votre portfolio apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4634,7 +4634,7 @@ Suivez votre évolution en temps réel grâce à une analyse approfondie et complète. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4650,7 +4650,7 @@ Configurer le compte apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5380,7 +5380,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5392,7 +5392,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5484,7 +5484,7 @@ Frais apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5492,7 +5492,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5716,7 +5716,7 @@ Extreme Peur libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5724,7 +5724,7 @@ Extreme Cupidité libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5732,7 +5732,7 @@ Neutre libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5748,7 +5748,7 @@ Confirmer la suppresion de ce message système? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5864,7 +5864,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5888,7 +5888,7 @@ Les données du marché sont retardées de apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5896,7 +5896,7 @@ Investissement apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5928,7 +5928,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5968,7 +5968,7 @@ Week to date libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5980,7 +5980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5988,7 +5988,7 @@ Month to date libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6000,7 +6000,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6008,7 +6008,7 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6056,7 +6056,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6068,7 +6068,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6091,12 +6091,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Général apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6104,7 +6112,7 @@ Cloud apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6116,7 +6124,7 @@ Self-Hosting apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6157,7 +6165,7 @@ Actif apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6165,7 +6173,7 @@ Clôturé apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6173,7 +6181,7 @@ Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6181,7 +6189,7 @@ Activitées apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6189,7 +6197,7 @@ Rendement en Dividende apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6221,7 +6229,7 @@ Liquiditées libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6264,6 +6272,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Zone de danger @@ -6285,7 +6301,7 @@ Par détention ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6293,7 +6309,7 @@ Approximation basée sur les principaux titres de chaque ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6325,7 +6341,7 @@ Voir plus libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Australie libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Autriche libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ Belgique libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bulgarie libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Canada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ République Tchèque libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finlande libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ France libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Allemagne libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ Inde libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ Italie libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Pays-Bas libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ Nouvelle-Zélande libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Pologne libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Roumanie libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ Afrique du Sud libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Thaïlande libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ Etats-Unis libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Inactif apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Oui libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Seuil Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Seuil Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ Accédez à plus de 80 000 symboles financiers issus de plus de 50 marchés boursiers. libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Ukraine libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Veuillez saisir votre clé API Ghostfolio : apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ Requêtes API aujourd’hui apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Arménie libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ Îles Vierges britanniques libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapour libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Générer un jeton de sécurité apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ Royaume-Uni libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ quelqu’un apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Voulez-vous vraiment supprimer cet élément? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Se déconnecter apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Le compte utilisateur de démonstration a été synchronisé. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Investissement alternatif libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Objet de collection libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index f75266b4d..7496bb8f9 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -139,7 +139,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -183,15 +183,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -303,7 +303,7 @@ Vuoi davvero eliminare questo account? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -331,11 +331,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -438,12 +438,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Data apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -487,7 +495,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -503,7 +511,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -511,7 +519,7 @@ Vuoi davvero eliminare questo buono? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -519,7 +527,7 @@ Vuoi davvero svuotare la cache? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -527,7 +535,7 @@ Imposta il messaggio di sistema: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -535,7 +543,7 @@ per utente apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -655,7 +663,7 @@ Partecipazione giornaliera apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -667,7 +675,7 @@ Ultima richiesta apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -683,7 +691,7 @@ Informazioni su Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -695,7 +703,7 @@ Inizia apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -727,11 +735,11 @@ Accedi apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -751,7 +759,7 @@ Ops! Token di sicurezza errato. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -923,7 +931,7 @@ Potere d’acquisto apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -931,7 +939,7 @@ Patrimonio netto apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -939,7 +947,7 @@ Prestazioni annualizzate apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -947,7 +955,7 @@ Inserisci l’importo del tuo fondo di emergenza: apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -963,7 +971,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -983,7 +991,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1047,7 +1055,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1059,7 +1067,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1071,7 +1079,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1083,7 +1091,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1103,7 +1111,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1119,7 +1127,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1135,7 +1143,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1167,7 +1175,7 @@ Il mio Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1355,7 +1363,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1363,11 +1371,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1423,7 +1431,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -1483,7 +1491,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1647,7 +1655,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -1675,7 +1683,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1683,11 +1691,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -1711,7 +1719,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -1727,7 +1735,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -1931,7 +1939,7 @@ Aggiungi un’attività apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1947,7 +1955,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -1967,7 +1975,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1975,11 +1983,11 @@ Quantità apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1995,7 +2003,7 @@ Prezzo unitario apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2015,7 +2023,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -2043,15 +2051,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2075,7 +2083,7 @@ Importazione dei dati... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2083,7 +2091,7 @@ L’importazione è stata completata apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2107,11 +2115,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2139,7 +2147,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -2179,7 +2187,7 @@ Iscrizione apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2219,7 +2227,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2267,7 +2275,7 @@ Importa le attività apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2391,7 +2399,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2407,7 +2415,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2419,7 +2427,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2447,7 +2455,7 @@ Prezzo massimo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -2483,7 +2491,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -2495,11 +2503,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2511,7 +2519,7 @@ Prezzo minimo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -2539,7 +2547,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2547,7 +2555,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2603,7 +2611,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -2619,7 +2627,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -2627,7 +2635,7 @@ Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -2703,7 +2711,7 @@ Escluso dall’analisi apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2747,7 +2755,7 @@ Importo totale apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -2771,7 +2779,7 @@ Tasso di risparmio apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2779,7 +2787,7 @@ Account apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2811,11 +2819,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2847,11 +2855,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -2859,7 +2867,7 @@ Etichetta libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -2871,11 +2879,11 @@ Contanti apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -2883,7 +2891,7 @@ Materia prima libs/ui/src/lib/i18n.ts - 47 + 46 @@ -2895,7 +2903,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -2903,7 +2911,7 @@ Reddito fisso libs/ui/src/lib/i18n.ts - 49 + 48 @@ -2911,7 +2919,7 @@ Immobiliare libs/ui/src/lib/i18n.ts - 51 + 50 @@ -2927,7 +2935,7 @@ Obbligazioni libs/ui/src/lib/i18n.ts - 54 + 53 @@ -2935,7 +2943,7 @@ Criptovaluta libs/ui/src/lib/i18n.ts - 57 + 56 @@ -2943,7 +2951,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -2951,7 +2959,7 @@ Fondo comune di investimento libs/ui/src/lib/i18n.ts - 60 + 59 @@ -2959,7 +2967,7 @@ Metalli preziosi libs/ui/src/lib/i18n.ts - 61 + 60 @@ -2967,7 +2975,7 @@ Azione ordinaria privata libs/ui/src/lib/i18n.ts - 62 + 61 @@ -2975,7 +2983,7 @@ Azione libs/ui/src/lib/i18n.ts - 63 + 62 @@ -2991,7 +2999,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -2999,7 +3007,7 @@ Altro libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3031,7 +3039,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3039,7 +3047,7 @@ Nord America libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3047,7 +3055,7 @@ Africa libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3055,7 +3063,7 @@ Asia libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3063,7 +3071,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3079,7 +3087,7 @@ Oceania libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3087,7 +3095,7 @@ Sud America libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3151,11 +3159,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3163,7 +3171,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3171,7 +3179,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3199,11 +3207,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3223,7 +3231,7 @@ Convalida dei dati... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3239,7 +3247,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3247,7 +3255,7 @@ Dati del mercato apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3311,7 +3319,7 @@ Importa i dividendi apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3347,7 +3355,7 @@ Sovvenzione libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3355,7 +3363,7 @@ Rischio più elevato libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3363,7 +3371,7 @@ Rischio inferiore libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3379,7 +3387,7 @@ Fondo pensione libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3395,7 +3403,7 @@ Satellite libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3667,7 +3675,7 @@ Commissioni apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3746,20 +3754,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Passa facilmente a Ghostfolio Open Source o a Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Loan Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3827,7 +3827,7 @@ Imita l’utente apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3835,7 +3835,7 @@ Elimina l’utente apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3943,7 +3943,7 @@ Aggiornamento del saldo di cassa apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3999,7 +3999,7 @@ Questa attività esiste già. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4079,7 +4079,7 @@ Mesi libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4087,7 +4087,7 @@ Anni libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4095,7 +4095,7 @@ Mese libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4103,7 +4103,7 @@ Anno libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4111,7 +4111,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4123,7 +4123,7 @@ Passività apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4263,7 +4263,7 @@ Passività libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4507,7 +4507,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4515,7 +4515,7 @@ Prezioso libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4539,7 +4539,7 @@ Asset apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4547,7 +4547,7 @@ Preimpostato libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4571,7 +4571,7 @@ Giappone libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4595,7 +4595,7 @@ Configura i tuoi account apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4603,7 +4603,7 @@ Ottieni una panoramica finanziaria completa aggiungendo i tuoi conti bancari e di trading. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4611,7 +4611,7 @@ Cattura le tue attività apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4619,7 +4619,7 @@ Registra le tue attività di investimento per tenere aggiornato il tuo portafoglio. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4627,7 +4627,7 @@ Monitora e analizza il tuo portafoglio apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4635,7 +4635,7 @@ Monitora i tuoi progressi in tempo reale, con analisi e approfondimenti completi. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4651,7 +4651,7 @@ Configura gli account apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5381,7 +5381,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5393,7 +5393,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5485,7 +5485,7 @@ Commissione apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5493,7 +5493,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5717,7 +5717,7 @@ Paura estrema libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5725,7 +5725,7 @@ Avidità estrema libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5733,7 +5733,7 @@ Neutrale libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5749,7 +5749,7 @@ Confermi di voler cancellare questo messaggio di sistema? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5865,7 +5865,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5889,7 +5889,7 @@ I dati di mercato sono ritardati di apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5897,7 +5897,7 @@ Investimento apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5929,7 +5929,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5969,7 +5969,7 @@ Da inizio settimana libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5981,7 +5981,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5989,7 +5989,7 @@ Da inizio mese libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6001,7 +6001,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6009,7 +6009,7 @@ Da inizio anno libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6057,7 +6057,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6069,7 +6069,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6092,12 +6092,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Generale apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6105,7 +6113,7 @@ Cloud apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6117,7 +6125,7 @@ Self-Hosting apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6158,7 +6166,7 @@ Attivo apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6166,7 +6174,7 @@ Chiuso apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6174,7 +6182,7 @@ Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6182,7 +6190,7 @@ Attività apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6190,7 +6198,7 @@ Rendimento da Dividendi apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6222,7 +6230,7 @@ Liquidità libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6265,6 +6273,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Zona di Pericolo @@ -6286,7 +6302,7 @@ Per ETF posseduti apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6294,7 +6310,7 @@ Approssimato in base ai principali ETF posseduti apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6326,7 +6342,7 @@ Visualizza di più libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6562,7 +6578,7 @@ Australia libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6570,7 +6586,7 @@ Austria libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6578,7 +6594,7 @@ Belgio libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6586,7 +6602,7 @@ Bulgaria libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6602,7 +6618,7 @@ Canada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6610,7 +6626,7 @@ Repubblica Ceca libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6618,7 +6634,7 @@ Finlandia libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6626,7 +6642,7 @@ Francia libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6634,7 +6650,7 @@ Germania libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6642,7 +6658,7 @@ India libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6650,7 +6666,7 @@ Italia libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6658,7 +6674,7 @@ Olanda libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6666,7 +6682,7 @@ Nuova Zelanda libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6674,7 +6690,7 @@ Polonia libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6682,7 +6698,7 @@ Romania libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6690,7 +6706,7 @@ Sud Africa libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6698,7 +6714,7 @@ Tailandia libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6706,7 +6722,7 @@ Stati Uniti libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6754,7 +6770,7 @@ Inattivo apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6798,7 +6814,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6838,7 +6854,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6854,7 +6870,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6874,7 +6890,7 @@ Si libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6922,7 +6938,7 @@ Soglia Minima apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6930,7 +6946,7 @@ Soglia Massima apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6962,7 +6978,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7142,7 +7158,7 @@ Ottieni accesso a oltre 80’000+ titoli da oltre 50 borse libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7150,7 +7166,7 @@ Ucraina libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7170,7 +7186,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7186,7 +7202,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7272,7 +7288,7 @@ Inserisci la tua API key di Ghostfolio: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7280,7 +7296,7 @@ Richieste API oggi apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7376,11 +7392,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7392,7 +7408,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7548,7 +7564,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7560,7 +7576,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7568,11 +7584,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7612,7 +7628,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7620,7 +7636,7 @@ Isole Vergini Britanniche libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7628,7 +7644,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7691,20 +7707,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Genera Token di Sicurezza apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7712,7 +7720,7 @@ United Kingdom libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7833,7 +7841,7 @@ qualcuno apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7865,7 +7873,7 @@ Vuoi davvero eliminare questo elemento? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7873,7 +7881,7 @@ Esci apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7906,7 +7914,7 @@ L’account utente demo è stato sincronizzato. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8333,7 +8341,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8349,7 +8357,7 @@ Investimenti alternativi libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8357,7 +8365,7 @@ Da collezione libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index 3b1036804..c0e3501a5 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -392,7 +392,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -448,7 +448,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -468,15 +468,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -588,7 +588,7 @@ 이 계정을 정말 삭제하시겠습니까? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -628,11 +628,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -719,12 +719,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date 날짜 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -796,7 +804,7 @@ 다음 기준으로 필터... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -812,7 +820,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -836,7 +844,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -920,7 +928,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -932,7 +940,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -944,11 +952,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -968,7 +976,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -988,7 +996,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1028,7 +1036,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -1072,7 +1080,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1080,7 +1088,7 @@ 이 쿠폰을 정말 삭제하시겠습니까? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -1088,7 +1096,7 @@ 이 시스템 메시지를 정말 삭제하시겠습니까? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -1096,7 +1104,7 @@ 정말로 캐시를 플러시하시겠습니까? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -1104,7 +1112,7 @@ 시스템 메시지를 설정하십시오: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1120,7 +1128,7 @@ 사용자당 apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1376,7 +1384,7 @@ 일일 참여 apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1388,7 +1396,7 @@ 마지막 요청 apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1396,7 +1404,7 @@ 사용자 대리 접속 apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1404,7 +1412,7 @@ 사용자 삭제 apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1448,7 +1456,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1484,7 +1492,7 @@ 고스트폴리오 소개 apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1496,11 +1504,11 @@ 로그인 apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1520,7 +1528,7 @@ 이런! 잘못된 보안 토큰. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1552,7 +1560,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1568,7 +1576,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1612,7 +1620,7 @@ 계정 설정 apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -1620,7 +1628,7 @@ 은행 및 중개 계좌를 추가하여 포괄적인 재무 개요를 확인하세요. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -1628,7 +1636,7 @@ 활동을 캡처하세요 apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -1636,7 +1644,7 @@ 투자 활동을 기록하여 포트폴리오를 최신 상태로 유지하세요. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -1644,7 +1652,7 @@ 포트폴리오를 모니터링하고 분석하세요. apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -1652,7 +1660,7 @@ 포괄적인 분석과 통찰력을 통해 진행 상황을 실시간으로 추적하세요. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -1660,7 +1668,7 @@ 계정 설정 apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1676,7 +1684,7 @@ 활동 추가 apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1688,7 +1696,7 @@ 총액 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1704,7 +1712,7 @@ 저축률 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1816,7 +1824,7 @@ 수수료 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1860,7 +1868,7 @@ 자산 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -1868,7 +1876,7 @@ 매수 가능 금액 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1876,7 +1884,7 @@ 분석에서 제외됨 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1884,7 +1892,7 @@ 부채 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -1896,7 +1904,7 @@ 순자산 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1904,7 +1912,7 @@ 연환산 성과 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1912,7 +1920,7 @@ 비상금 금액을 설정해 주세요. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1920,7 +1928,7 @@ 최저가 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1928,7 +1936,7 @@ 최고가 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1936,11 +1944,11 @@ 수량 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2136,7 +2144,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -2148,7 +2156,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -2160,7 +2168,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2172,7 +2180,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2192,7 +2200,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2488,7 +2496,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2500,7 +2508,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2516,7 +2524,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2532,7 +2540,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2632,7 +2640,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2640,11 +2648,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2728,7 +2736,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -2740,7 +2748,7 @@ 시장 데이터 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -2792,7 +2800,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2800,11 +2808,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2992,7 +3000,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -3112,7 +3120,7 @@ 시작하기 apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -3192,7 +3200,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -3208,7 +3216,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -3692,15 +3700,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3792,7 +3800,7 @@ 현금 잔액 업데이트 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3800,7 +3808,7 @@ 단가 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3812,7 +3820,7 @@ 활동 가져오기 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3828,7 +3836,7 @@ 배당금 가져오기 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3844,7 +3852,7 @@ 데이터 가져오는 중... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -3852,7 +3860,7 @@ 가져오기가 완료되었습니다. apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -3868,7 +3876,7 @@ 데이터 유효성을 검사하는 중... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -4052,7 +4060,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -4152,11 +4160,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -4164,7 +4172,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -4172,7 +4180,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -4292,11 +4300,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -4536,7 +4544,7 @@ 등록 apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -4861,7 +4869,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -4873,7 +4881,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -4889,7 +4897,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4937,7 +4945,7 @@ 나의 고스트폴리오 apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -5105,7 +5113,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -5157,7 +5165,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -5165,7 +5173,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -5209,7 +5217,7 @@ 계정 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5249,11 +5257,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -5281,11 +5289,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -5316,14 +5324,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Ghostfolio Open Source 또는 Ghostfolio Basic으로 쉽게 전환하세요 - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund 비상자금 @@ -5337,7 +5337,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -5345,7 +5345,7 @@ 승인하다 libs/ui/src/lib/i18n.ts - 19 + 18 @@ -5353,7 +5353,7 @@ 더 높은 위험 libs/ui/src/lib/i18n.ts - 20 + 19 @@ -5361,7 +5361,7 @@ 이 활동은 이미 존재합니다. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -5369,7 +5369,7 @@ 일본 libs/ui/src/lib/i18n.ts - 93 + 92 @@ -5377,7 +5377,7 @@ 위험 감소 libs/ui/src/lib/i18n.ts - 22 + 21 @@ -5385,7 +5385,7 @@ libs/ui/src/lib/i18n.ts - 23 + 22 @@ -5393,7 +5393,7 @@ 개월 libs/ui/src/lib/i18n.ts - 24 + 23 @@ -5401,7 +5401,7 @@ 다른 libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -5413,7 +5413,7 @@ 프리셋 libs/ui/src/lib/i18n.ts - 27 + 26 @@ -5429,7 +5429,7 @@ 퇴직금 libs/ui/src/lib/i18n.ts - 28 + 27 @@ -5445,7 +5445,7 @@ 위성 libs/ui/src/lib/i18n.ts - 29 + 28 @@ -5469,11 +5469,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -5481,7 +5481,7 @@ 꼬리표 libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -5493,7 +5493,7 @@ 년도 libs/ui/src/lib/i18n.ts - 32 + 31 @@ -5501,7 +5501,7 @@ 세부정보 보기 apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -5513,7 +5513,7 @@ 연령 libs/ui/src/lib/i18n.ts - 33 + 32 @@ -5533,7 +5533,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -5541,7 +5541,7 @@ 요금 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5549,7 +5549,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5557,7 +5557,7 @@ 귀중한 libs/ui/src/lib/i18n.ts - 43 + 42 @@ -5565,7 +5565,7 @@ 책임 libs/ui/src/lib/i18n.ts - 41 + 40 @@ -5577,7 +5577,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -5585,11 +5585,11 @@ 현금 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -5597,7 +5597,7 @@ 상품 libs/ui/src/lib/i18n.ts - 47 + 46 @@ -5609,7 +5609,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -5617,7 +5617,7 @@ 채권 libs/ui/src/lib/i18n.ts - 49 + 48 @@ -5625,7 +5625,7 @@ 부동산 libs/ui/src/lib/i18n.ts - 51 + 50 @@ -5641,7 +5641,7 @@ 노예 libs/ui/src/lib/i18n.ts - 54 + 53 @@ -5649,7 +5649,7 @@ 암호화폐 libs/ui/src/lib/i18n.ts - 57 + 56 @@ -5657,7 +5657,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -5665,7 +5665,7 @@ 뮤추얼 펀드 libs/ui/src/lib/i18n.ts - 60 + 59 @@ -5673,7 +5673,7 @@ 귀금속 libs/ui/src/lib/i18n.ts - 61 + 60 @@ -5681,7 +5681,7 @@ 사모펀드 libs/ui/src/lib/i18n.ts - 62 + 61 @@ -5689,7 +5689,7 @@ 재고 libs/ui/src/lib/i18n.ts - 63 + 62 @@ -5697,7 +5697,7 @@ 아프리카 libs/ui/src/lib/i18n.ts - 70 + 69 @@ -5705,7 +5705,7 @@ 아시아 libs/ui/src/lib/i18n.ts - 71 + 70 @@ -5713,7 +5713,7 @@ 유럽 libs/ui/src/lib/i18n.ts - 72 + 71 @@ -5721,7 +5721,7 @@ 북미 libs/ui/src/lib/i18n.ts - 73 + 72 @@ -5737,7 +5737,7 @@ 오세아니아 libs/ui/src/lib/i18n.ts - 74 + 73 @@ -5745,7 +5745,7 @@ 남아메리카 libs/ui/src/lib/i18n.ts - 75 + 74 @@ -5753,7 +5753,7 @@ 극심한 공포 libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5761,7 +5761,7 @@ 극도의 탐욕 libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5769,7 +5769,7 @@ 중립적 libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5817,7 +5817,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -5865,7 +5865,7 @@ 아르헨티나 libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5913,7 +5913,7 @@ 시장 데이터가 지연됩니다. apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5929,7 +5929,7 @@ 닫기 보유 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5945,7 +5945,7 @@ 투자 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5993,7 +5993,7 @@ 연초 현재 libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6001,7 +6001,7 @@ 이번주 현재까지 libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6009,7 +6009,7 @@ 월간 누계 libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6021,7 +6021,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6033,7 +6033,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6081,7 +6081,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6093,7 +6093,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6122,7 +6122,7 @@ 셀프 호스팅 apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6141,12 +6141,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General 일반적인 apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6154,7 +6162,7 @@ 구름 apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6182,7 +6190,7 @@ 닫은 apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6190,7 +6198,7 @@ 활동적인 apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6198,7 +6206,7 @@ 인도네시아 공화국 libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6206,7 +6214,7 @@ 활동 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6214,7 +6222,7 @@ 배당수익률 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6246,7 +6254,7 @@ 유동성 libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6297,6 +6305,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone 위험지대 @@ -6310,7 +6326,7 @@ 각 ETF의 상위 보유량을 기준으로 한 근사치 apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6318,7 +6334,7 @@ ETF 홀딩으로 apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6350,7 +6366,7 @@ 더 보기 libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6482,7 +6498,7 @@ 태국 libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6490,7 +6506,7 @@ 인도 libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6498,7 +6514,7 @@ 오스트리아 libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6506,7 +6522,7 @@ 폴란드 libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6514,7 +6530,7 @@ 이탈리아 libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6570,7 +6586,7 @@ 캐나다 libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6578,7 +6594,7 @@ 뉴질랜드 libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6586,7 +6602,7 @@ 네덜란드 libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6630,7 +6646,7 @@ 루마니아 libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6638,7 +6654,7 @@ 독일 libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6646,7 +6662,7 @@ 미국 libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6662,7 +6678,7 @@ 벨기에 libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6682,7 +6698,7 @@ 체코 libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6690,7 +6706,7 @@ 호주 libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6698,7 +6714,7 @@ 남아프리카 libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6706,7 +6722,7 @@ 불가리아 libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6722,7 +6738,7 @@ 핀란드 libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6730,7 +6746,7 @@ 프랑스 libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6782,7 +6798,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6810,7 +6826,7 @@ libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6826,7 +6842,7 @@ 비활성 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6854,7 +6870,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6870,7 +6886,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6906,7 +6922,7 @@ 임계값 최대 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6938,7 +6954,7 @@ 임계값 최소 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6994,7 +7010,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7150,7 +7166,7 @@ 우크라이나 libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7166,7 +7182,7 @@ 50개 이상의 거래소에서 80,000개 이상의 티커에 접근하세요 libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7194,7 +7210,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7223,7 +7239,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7264,7 +7280,7 @@ Ghostfolio API 키를 입력하세요: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7320,7 +7336,7 @@ 오늘의 API 요청 apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7400,11 +7416,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7416,7 +7432,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7572,7 +7588,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7584,7 +7600,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7592,11 +7608,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7620,7 +7636,7 @@ 싱가포르 libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7636,7 +7652,7 @@ 아르메니아 libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7644,7 +7660,7 @@ 영국령 버진아일랜드 libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7703,14 +7719,6 @@ 240 - - Find account, holding or page... - 계정, 보유 또는 페이지 찾기... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Security token 보안 토큰 @@ -7728,7 +7736,7 @@ 보안 토큰 생성 apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7736,7 +7744,7 @@ 영국 libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7833,7 +7841,7 @@ 누구 apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7865,7 +7873,7 @@ 이 항목을 정말로 삭제하시겠습니까? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7873,7 +7881,7 @@ 로그아웃 apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7914,7 +7922,7 @@ 데모 사용자 계정이 동기화되었습니다. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8333,7 +8341,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8349,7 +8357,7 @@ 대체투자 libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8357,7 +8365,7 @@ 소장용 libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 3f51840c9..b33aee404 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -138,7 +138,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -182,15 +182,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -302,7 +302,7 @@ Wil je deze rekening echt verwijderen? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -330,11 +330,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -437,12 +437,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Datum apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -486,7 +494,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -502,7 +510,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -510,7 +518,7 @@ Wil je deze coupon echt verwijderen? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -518,7 +526,7 @@ Wil je echt de cache legen? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -526,7 +534,7 @@ Stel je systeemboodschap in: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -534,7 +542,7 @@ per gebruiker apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -654,7 +662,7 @@ Betrokkenheid per dag apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -666,7 +674,7 @@ Laatste verzoek apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -682,7 +690,7 @@ Over Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -694,7 +702,7 @@ Aan de slag apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -726,11 +734,11 @@ Aanmelden apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -750,7 +758,7 @@ Oeps! Onjuiste beveiligingstoken. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -922,7 +930,7 @@ Koopkracht apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -930,7 +938,7 @@ Netto waarde apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -938,7 +946,7 @@ Rendement per jaar apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -946,7 +954,7 @@ Voer het bedrag van je noodfonds in: apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -962,7 +970,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -982,7 +990,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1046,7 +1054,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1058,7 +1066,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1070,7 +1078,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1082,7 +1090,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1102,7 +1110,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1118,7 +1126,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1134,7 +1142,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1166,7 +1174,7 @@ Mijn Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1354,7 +1362,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1362,11 +1370,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1422,7 +1430,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -1482,7 +1490,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1646,7 +1654,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -1674,7 +1682,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1682,11 +1690,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -1710,7 +1718,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -1726,7 +1734,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -1930,7 +1938,7 @@ Activiteit toevoegen apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1946,7 +1954,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -1966,7 +1974,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1974,11 +1982,11 @@ Hoeveelheid apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1994,7 +2002,7 @@ Prijs per eenheid apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2014,7 +2022,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -2042,15 +2050,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2074,7 +2082,7 @@ Gegevens importeren... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2082,7 +2090,7 @@ Importeren is voltooid apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2106,11 +2114,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2138,7 +2146,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -2178,7 +2186,7 @@ Registratie apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2218,7 +2226,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2266,7 +2274,7 @@ Activiteiten importeren apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2390,7 +2398,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2406,7 +2414,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2418,7 +2426,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2446,7 +2454,7 @@ Maximale prijs apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -2482,7 +2490,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -2494,11 +2502,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2510,7 +2518,7 @@ Minimale prijs apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -2538,7 +2546,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2546,7 +2554,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2602,7 +2610,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -2618,7 +2626,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -2626,7 +2634,7 @@ Filter op... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -2702,7 +2710,7 @@ Uitgesloten van analyse apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2746,7 +2754,7 @@ Totaalbedrag apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -2770,7 +2778,7 @@ Spaarrente apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2778,7 +2786,7 @@ Account apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2810,11 +2818,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2846,11 +2854,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -2858,7 +2866,7 @@ Label libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -2870,11 +2878,11 @@ Contant geld apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -2882,7 +2890,7 @@ Grondstof libs/ui/src/lib/i18n.ts - 47 + 46 @@ -2894,7 +2902,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -2902,7 +2910,7 @@ Vast inkomen libs/ui/src/lib/i18n.ts - 49 + 48 @@ -2910,7 +2918,7 @@ Vastgoed libs/ui/src/lib/i18n.ts - 51 + 50 @@ -2926,7 +2934,7 @@ Obligatie libs/ui/src/lib/i18n.ts - 54 + 53 @@ -2934,7 +2942,7 @@ Cryptovaluta libs/ui/src/lib/i18n.ts - 57 + 56 @@ -2942,7 +2950,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -2950,7 +2958,7 @@ Beleggingsfonds libs/ui/src/lib/i18n.ts - 60 + 59 @@ -2958,7 +2966,7 @@ Edelmetaal libs/ui/src/lib/i18n.ts - 61 + 60 @@ -2966,7 +2974,7 @@ Private equity libs/ui/src/lib/i18n.ts - 62 + 61 @@ -2974,7 +2982,7 @@ Aandeel libs/ui/src/lib/i18n.ts - 63 + 62 @@ -2990,7 +2998,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -2998,7 +3006,7 @@ Anders libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3030,7 +3038,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3038,7 +3046,7 @@ Noord-Amerika libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3046,7 +3054,7 @@ Afrika libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3054,7 +3062,7 @@ Azië libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3062,7 +3070,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3078,7 +3086,7 @@ Oceanië libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3086,7 +3094,7 @@ Zuid-Amerika libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3150,11 +3158,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3162,7 +3170,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3170,7 +3178,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3198,11 +3206,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3222,7 +3230,7 @@ Gegevens valideren... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3238,7 +3246,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3246,7 +3254,7 @@ Marktgegevens apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3310,7 +3318,7 @@ Importeer dividenden apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3346,7 +3354,7 @@ Toelage libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3354,7 +3362,7 @@ Hoger risico libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3362,7 +3370,7 @@ Lager risico libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3378,7 +3386,7 @@ Pensioen libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3394,7 +3402,7 @@ Satelliet libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3666,7 +3674,7 @@ Kosten apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3745,20 +3753,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Eenvoudig overstappen naar Ghostfolio Open Source of Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Loan Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3826,7 +3826,7 @@ Gebruiker immiteren apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3834,7 +3834,7 @@ Gebruiker verwijderen apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3942,7 +3942,7 @@ Saldo bijwerken apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3998,7 +3998,7 @@ Deze activiteit bestaat al. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4078,7 +4078,7 @@ Maanden libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4086,7 +4086,7 @@ Jaren libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4094,7 +4094,7 @@ Maand libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4102,7 +4102,7 @@ Jaar libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4110,7 +4110,7 @@ Bekijk details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4122,7 +4122,7 @@ Verplichtingen apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4262,7 +4262,7 @@ Verplichtingen libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4506,7 +4506,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4514,7 +4514,7 @@ Waardevol libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4538,7 +4538,7 @@ Assets apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4546,7 +4546,7 @@ Voorinstelling libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4570,7 +4570,7 @@ Japan libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4594,7 +4594,7 @@ Je accounts instellen apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4602,7 +4602,7 @@ Krijg een uitgebreid financieel overzicht door je bank- en effectenrekeningen toe te voegen. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4610,7 +4610,7 @@ Leg je activiteiten vast apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4618,7 +4618,7 @@ Leg je investeringsactiviteiten vast om je portefeuille up-to-date te houden. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4626,7 +4626,7 @@ Volg en analyseer je portfolio apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4634,7 +4634,7 @@ Volg je voortgang in real-time met uitgebreide analyses en inzichten. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4650,7 +4650,7 @@ Account opzetten apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5380,7 +5380,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5392,7 +5392,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5484,7 +5484,7 @@ Kosten apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5492,7 +5492,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5716,7 +5716,7 @@ Extreme Angst libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5724,7 +5724,7 @@ Extreme Hebzucht libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5732,7 +5732,7 @@ Neutraal libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5748,7 +5748,7 @@ Wilt u dit systeembericht echt verwijderen? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5864,7 +5864,7 @@ Argentinië libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5888,7 +5888,7 @@ Markt data is vertraagd voor apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5896,7 +5896,7 @@ Investering apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5928,7 +5928,7 @@ Sluit Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5968,7 +5968,7 @@ Week tot nu toe libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5980,7 +5980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5988,7 +5988,7 @@ Maand tot nu toe libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6000,7 +6000,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6008,7 +6008,7 @@ Jaar tot nu toe libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6056,7 +6056,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6068,7 +6068,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6091,12 +6091,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Algemeen apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6104,7 +6112,7 @@ Cloud apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6116,7 +6124,7 @@ Zelf Hosten apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6157,7 +6165,7 @@ Actief apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6165,7 +6173,7 @@ Gesloten apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6173,7 +6181,7 @@ Indonesië libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6181,7 +6189,7 @@ Activiteit apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6189,7 +6197,7 @@ Dividendrendement apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6221,7 +6229,7 @@ Liquiditeit libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6264,6 +6272,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Gevarenzone @@ -6285,7 +6301,7 @@ Per Aangehouden ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6293,7 +6309,7 @@ Benadering op basis van de grootste belegingen binnen iedere ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6325,7 +6341,7 @@ Laat meer zien libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Australië libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Oostenrijk libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ België libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bulgarije libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Canada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ Tsjechische Republiek libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finland libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ Frankrijk libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Duitsland libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ India libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ Italië libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Nederland libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ Nieuw-Zeeland libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Polen libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Roemenië libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ Zuid-Afrika libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Thailand libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ Verenigde Station libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Inactief apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Ja libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Drempelwaarde Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Drempelwaarde Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ Krijg toegang tot meer dan 80.000 tickers van meer dan 50 beurzen libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Oekraïne libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Voer uw Ghostfolio API-sleutel in: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ Aantal API-Verzoeken Vandaag apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Armenië libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ Britse Maagdeneilanden libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Vindt een account, holding of pagina... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Beveiligingstoken Aanmaken apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ Verenigd Koninkrijk libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ iemand apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Wilt u dit item echt verwijderen? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Uitloggen apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo-gebruikersaccount is gesynchroniseerd. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Alternatieve belegging libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Verzamelobject libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index a47b1d7bf..b48149e75 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -383,7 +383,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -439,7 +439,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -459,15 +459,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -579,7 +579,7 @@ Czy na pewno chcesz usunąć to konto? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -619,11 +619,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -710,12 +710,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Data apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -787,7 +795,7 @@ Filtruj według... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -803,7 +811,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -827,7 +835,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -887,7 +895,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -899,7 +907,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -911,11 +919,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -935,7 +943,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -955,7 +963,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -995,7 +1003,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -1039,7 +1047,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1047,7 +1055,7 @@ Czy naprawdę chcesz usunąć ten kupon? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -1055,7 +1063,7 @@ Czy naprawdę chcesz usunąć tę wiadomość systemową? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -1063,7 +1071,7 @@ Czy naprawdę chcesz wyczyścić pamięć podręczną? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -1071,7 +1079,7 @@ Proszę ustawić swoją wiadomość systemową: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1087,7 +1095,7 @@ na Użytkownika apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1343,7 +1351,7 @@ Zaangażowanie na Dzień apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1355,7 +1363,7 @@ Ostatnie Żądanie apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1363,7 +1371,7 @@ Wciel się w Użytkownika apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1371,7 +1379,7 @@ Usuń Użytkownika apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1415,7 +1423,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1451,7 +1459,7 @@ O Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1463,11 +1471,11 @@ Zaloguj się apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1487,7 +1495,7 @@ Ups! Nieprawidłowy token bezpieczeństwa. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1519,7 +1527,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1535,7 +1543,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1579,7 +1587,7 @@ Skonfiguruj swoje konta apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -1587,7 +1595,7 @@ Uzyskaj kompleksowy przegląd finansowy, poprzez dodanie swoich rachunków bankowych i maklerskich. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -1595,7 +1603,7 @@ Rejestruj swoje działania apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -1603,7 +1611,7 @@ Dokumentuj swoje działania inwestycyjne, aby zapewnić aktualność portfela. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -1611,7 +1619,7 @@ Monitoruj i analizuj swój portfel apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -1619,7 +1627,7 @@ Śledź swój postęp w czasie rzeczywistym dzięki kompleksowym analizom i obserwacjom. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -1627,7 +1635,7 @@ Konfiguracja kont apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1643,7 +1651,7 @@ Dodaj działalność apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1655,7 +1663,7 @@ Całkowita Kwota apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1671,7 +1679,7 @@ Stopa Oszczędności apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1783,7 +1791,7 @@ Opłaty apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1827,7 +1835,7 @@ Aktywa apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -1835,7 +1843,7 @@ Siła Nabywcza apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1843,7 +1851,7 @@ Wykluczone z Analizy apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1851,7 +1859,7 @@ Pasywa (Zobowiązania Finansowe) apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -1863,7 +1871,7 @@ Wartość Netto apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1871,7 +1879,7 @@ Osiągi w Ujęciu Rocznym apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1879,7 +1887,7 @@ Wprowadź wysokość funduszu rezerwowego: apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1887,7 +1895,7 @@ Cena Minimalna apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1895,7 +1903,7 @@ Cena Maksymalna apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1903,11 +1911,11 @@ Ilość apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2103,7 +2111,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -2115,7 +2123,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -2127,7 +2135,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2139,7 +2147,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2159,7 +2167,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2455,7 +2463,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2467,7 +2475,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2483,7 +2491,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2499,7 +2507,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2599,7 +2607,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2607,11 +2615,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2695,7 +2703,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -2707,7 +2715,7 @@ Dane Rynkowe apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -2759,7 +2767,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2767,11 +2775,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2959,7 +2967,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -3079,7 +3087,7 @@ Rozpocznij apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -3159,7 +3167,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -3175,7 +3183,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -3659,15 +3667,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3759,7 +3767,7 @@ Zaktualizuj Saldo Gotówkowe apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3767,7 +3775,7 @@ Cena Jednostkowa apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3779,7 +3787,7 @@ Importuj Aktywności apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3795,7 +3803,7 @@ Impotruj Dywidendy apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3811,7 +3819,7 @@ Importowanie danych... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -3819,7 +3827,7 @@ Importowanie zakończone apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -3835,7 +3843,7 @@ Weryfikacja danych... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -4019,7 +4027,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -4119,11 +4127,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -4131,7 +4139,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -4139,7 +4147,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -4259,11 +4267,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -4503,7 +4511,7 @@ Rejestracja apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -4816,7 +4824,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -4828,7 +4836,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -4844,7 +4852,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4892,7 +4900,7 @@ Moje Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -5036,7 +5044,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -5088,7 +5096,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -5096,7 +5104,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -5140,7 +5148,7 @@ Konto apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5180,11 +5188,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -5212,11 +5220,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -5247,14 +5255,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Przełącz się z łatwością na Ghostfolio Open Source lub Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund Fundusz Rezerwowy @@ -5268,7 +5268,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -5276,7 +5276,7 @@ Dotacja libs/ui/src/lib/i18n.ts - 19 + 18 @@ -5284,7 +5284,7 @@ Wyższe Ryzyko libs/ui/src/lib/i18n.ts - 20 + 19 @@ -5292,7 +5292,7 @@ Ta działalność już istnieje. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -5300,7 +5300,7 @@ Japonia libs/ui/src/lib/i18n.ts - 93 + 92 @@ -5308,7 +5308,7 @@ Niższe Ryzyko libs/ui/src/lib/i18n.ts - 22 + 21 @@ -5316,7 +5316,7 @@ Miesiąc libs/ui/src/lib/i18n.ts - 23 + 22 @@ -5324,7 +5324,7 @@ Miesiące libs/ui/src/lib/i18n.ts - 24 + 23 @@ -5332,7 +5332,7 @@ Inne libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -5344,7 +5344,7 @@ Wstępnie ustawione libs/ui/src/lib/i18n.ts - 27 + 26 @@ -5360,7 +5360,7 @@ Świadczenia Emerytalne libs/ui/src/lib/i18n.ts - 28 + 27 @@ -5376,7 +5376,7 @@ Satelita libs/ui/src/lib/i18n.ts - 29 + 28 @@ -5400,11 +5400,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -5412,7 +5412,7 @@ Tag libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -5424,7 +5424,7 @@ Rok libs/ui/src/lib/i18n.ts - 32 + 31 @@ -5432,7 +5432,7 @@ Zobacz szczegóły apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -5444,7 +5444,7 @@ Lata libs/ui/src/lib/i18n.ts - 33 + 32 @@ -5464,7 +5464,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -5472,7 +5472,7 @@ Opłata apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5480,7 +5480,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5488,7 +5488,7 @@ Kosztowności libs/ui/src/lib/i18n.ts - 43 + 42 @@ -5496,7 +5496,7 @@ Zobowiązanie libs/ui/src/lib/i18n.ts - 41 + 40 @@ -5508,7 +5508,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -5516,11 +5516,11 @@ Gotówka apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -5528,7 +5528,7 @@ Towar libs/ui/src/lib/i18n.ts - 47 + 46 @@ -5540,7 +5540,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -5548,7 +5548,7 @@ Stały Dochód libs/ui/src/lib/i18n.ts - 49 + 48 @@ -5556,7 +5556,7 @@ Nieruchomość libs/ui/src/lib/i18n.ts - 51 + 50 @@ -5572,7 +5572,7 @@ Obligacja libs/ui/src/lib/i18n.ts - 54 + 53 @@ -5580,7 +5580,7 @@ Kryptowaluta libs/ui/src/lib/i18n.ts - 57 + 56 @@ -5588,7 +5588,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -5596,7 +5596,7 @@ Fundusz Wzajemny libs/ui/src/lib/i18n.ts - 60 + 59 @@ -5604,7 +5604,7 @@ Metal Szlachetny libs/ui/src/lib/i18n.ts - 61 + 60 @@ -5612,7 +5612,7 @@ Prywatny Kapitał libs/ui/src/lib/i18n.ts - 62 + 61 @@ -5620,7 +5620,7 @@ Akcje libs/ui/src/lib/i18n.ts - 63 + 62 @@ -5628,7 +5628,7 @@ Afryka libs/ui/src/lib/i18n.ts - 70 + 69 @@ -5636,7 +5636,7 @@ Azja libs/ui/src/lib/i18n.ts - 71 + 70 @@ -5644,7 +5644,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -5652,7 +5652,7 @@ Ameryka Północna libs/ui/src/lib/i18n.ts - 73 + 72 @@ -5668,7 +5668,7 @@ Oceania libs/ui/src/lib/i18n.ts - 74 + 73 @@ -5676,7 +5676,7 @@ Ameryka Południowa libs/ui/src/lib/i18n.ts - 75 + 74 @@ -5684,7 +5684,7 @@ Skrajny Strach libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5692,7 +5692,7 @@ Skrajna Zachłanność libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5700,7 +5700,7 @@ Neutralny libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5748,7 +5748,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -5864,7 +5864,7 @@ Argentyna libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5888,7 +5888,7 @@ Dane rynkowe są opóźnione o apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5896,7 +5896,7 @@ Inwestycje apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5928,7 +5928,7 @@ Zamknij pozycję apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5968,7 +5968,7 @@ Dotychczasowy tydzień libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5980,7 +5980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5988,7 +5988,7 @@ Od początku miesiąca libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6000,7 +6000,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6008,7 +6008,7 @@ Od początku roku libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6056,7 +6056,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6068,7 +6068,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6091,12 +6091,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Informacje Ogólne apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6104,7 +6112,7 @@ Rozwiązanie w Chmurze apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6116,7 +6124,7 @@ Własny Hosting apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6157,7 +6165,7 @@ Antywne apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6165,7 +6173,7 @@ Zamknięte apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6173,7 +6181,7 @@ Indonezja libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6181,7 +6189,7 @@ Aktywność apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6189,7 +6197,7 @@ Dochód z Dywidendy apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6221,7 +6229,7 @@ Płynność środków finansowych libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6264,6 +6272,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Strefa Zagrożenia @@ -6285,7 +6301,7 @@ Wg. Holdingu ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6293,7 +6309,7 @@ Przybliżenie oparte na najwyższych aktywach każdego funduszu ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6325,7 +6341,7 @@ Pokaż więcej libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Australia libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Austria libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ Belgia libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bułgaria libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Kanada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ Czechy libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finlandia libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ Francja libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Niemcy libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ Indie libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ Włochy libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Holandia libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ Nowa Zelandia libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Polska libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Rumunia libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ Południowa Afryka libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Tajlandia libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ Stany Zjednoczone libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Nieaktywny apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Tak libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Próg minimalny apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Próg maksymalny apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ Uzyskaj dostęp do ponad 80 000 pasków notowań giełdowych z ponad 50 giełd libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Ukraina libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Wprowadź swój klucz API konta Ghostfolio: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ Dzisiejsze Zapytania API apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ Brytyjskie Wyspy Dziewicze libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapur libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Znajdź konto, pozycję lub stronę... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Generowanie Tokena Zabezpieczającego apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ Wielka Brytania libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ ktoś apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Czy na pewno chcesz usunąć ten element? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Wyloguj się apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Konto użytkownika demonstracyjnego zostało zsynchronizowane. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Inwestycja alternatywna libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Kolekcjonerskie libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 179281bc6..2588a73ab 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -146,7 +146,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -202,7 +202,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -238,15 +238,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -358,7 +358,7 @@ Pretende realmente eliminar esta conta? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -378,11 +378,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -493,12 +493,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Data apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -534,7 +542,7 @@ Filtrar por... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -550,7 +558,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -566,7 +574,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -602,7 +610,7 @@ Deseja realmente eliminar este cupão? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -610,7 +618,7 @@ Deseja realmente limpar a cache? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -618,7 +626,7 @@ Por favor, defina a sua mensagem do sistema: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -626,7 +634,7 @@ por Utilizador apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -734,7 +742,7 @@ Envolvimento por Dia apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -746,7 +754,7 @@ Último Pedido apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -782,7 +790,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -818,7 +826,7 @@ Sobre o Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -830,11 +838,11 @@ Iniciar sessão apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -854,7 +862,7 @@ Oops! Token de Segurança Incorreto. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -886,7 +894,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -902,7 +910,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -938,7 +946,7 @@ Valor Total apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -954,7 +962,7 @@ Taxa de Poupança apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1098,7 +1106,7 @@ Poder de Compra apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1106,7 +1114,7 @@ Excluído da Análise apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1114,7 +1122,7 @@ Valor Líquido apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1122,7 +1130,7 @@ Desempenho Anual apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1130,7 +1138,7 @@ Por favor, insira o valor do seu fundo de emergência: apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1138,7 +1146,7 @@ Preço Mínimo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1146,7 +1154,7 @@ Preço Máximo apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1154,11 +1162,11 @@ Quantidade apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1178,7 +1186,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -1190,11 +1198,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1214,7 +1222,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -1234,7 +1242,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1298,7 +1306,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1310,7 +1318,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1322,7 +1330,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1334,7 +1342,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -1354,7 +1362,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -1378,7 +1386,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -1390,7 +1398,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -1406,7 +1414,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -1422,7 +1430,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -1454,7 +1462,7 @@ O meu Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -1706,7 +1714,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -1714,11 +1722,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1770,7 +1778,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -1934,7 +1942,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -1962,7 +1970,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -1970,11 +1978,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -1998,7 +2006,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -2014,7 +2022,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -2058,15 +2066,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2106,7 +2114,7 @@ Adicionar atividade apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -2122,7 +2130,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -2142,7 +2150,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -2150,7 +2158,7 @@ Preço por Unidade apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2170,7 +2178,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -2178,7 +2186,7 @@ A importar dados... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -2186,7 +2194,7 @@ A importação foi concluída apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -2318,7 +2326,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -2474,11 +2482,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -2530,7 +2538,7 @@ Começar apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -2562,7 +2570,7 @@ Registo apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -2622,7 +2630,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -2670,7 +2678,7 @@ Importar Atividades apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2778,7 +2786,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2786,7 +2794,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -2802,7 +2810,7 @@ Conta apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2834,11 +2842,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -2862,7 +2870,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -2870,7 +2878,7 @@ Outro libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -2898,11 +2906,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -2910,7 +2918,7 @@ Marcador libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -2922,11 +2930,11 @@ Dinheiro apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -2934,7 +2942,7 @@ Matéria-prima libs/ui/src/lib/i18n.ts - 47 + 46 @@ -2946,7 +2954,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -2954,7 +2962,7 @@ Rendimento Fixo libs/ui/src/lib/i18n.ts - 49 + 48 @@ -2962,7 +2970,7 @@ Imobiliário libs/ui/src/lib/i18n.ts - 51 + 50 @@ -2978,7 +2986,7 @@ Obrigação libs/ui/src/lib/i18n.ts - 54 + 53 @@ -2986,7 +2994,7 @@ Criptomoedas libs/ui/src/lib/i18n.ts - 57 + 56 @@ -2994,7 +3002,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -3002,7 +3010,7 @@ Fundo de Investimento libs/ui/src/lib/i18n.ts - 60 + 59 @@ -3010,7 +3018,7 @@ Metal Precioso libs/ui/src/lib/i18n.ts - 61 + 60 @@ -3018,7 +3026,7 @@ Private Equity libs/ui/src/lib/i18n.ts - 62 + 61 @@ -3026,7 +3034,7 @@ Ação libs/ui/src/lib/i18n.ts - 63 + 62 @@ -3034,7 +3042,7 @@ África libs/ui/src/lib/i18n.ts - 70 + 69 @@ -3042,7 +3050,7 @@ Ásia libs/ui/src/lib/i18n.ts - 71 + 70 @@ -3050,7 +3058,7 @@ Europa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -3058,7 +3066,7 @@ América do Norte libs/ui/src/lib/i18n.ts - 73 + 72 @@ -3074,7 +3082,7 @@ Oceânia libs/ui/src/lib/i18n.ts - 74 + 73 @@ -3082,7 +3090,7 @@ América do Sul libs/ui/src/lib/i18n.ts - 75 + 74 @@ -3118,7 +3126,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -3158,7 +3166,7 @@ Dados de Mercado apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3182,7 +3190,7 @@ A validar dados... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3198,7 +3206,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3218,11 +3226,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3230,7 +3238,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3238,7 +3246,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3266,11 +3274,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -3310,7 +3318,7 @@ Importar Dividendos apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3346,7 +3354,7 @@ Conceder libs/ui/src/lib/i18n.ts - 19 + 18 @@ -3354,7 +3362,7 @@ Risco mais Elevado libs/ui/src/lib/i18n.ts - 20 + 19 @@ -3362,7 +3370,7 @@ Risco menos Elevado libs/ui/src/lib/i18n.ts - 22 + 21 @@ -3378,7 +3386,7 @@ Provisão de Reforma libs/ui/src/lib/i18n.ts - 28 + 27 @@ -3394,7 +3402,7 @@ Satélite libs/ui/src/lib/i18n.ts - 29 + 28 @@ -3666,7 +3674,7 @@ Taxas apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -3745,20 +3753,12 @@ 12 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Mude para o Ghostfolio Open Source ou Ghostfolio Basic facilmente - - libs/ui/src/lib/i18n.ts - 14 - - Loan Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -3826,7 +3826,7 @@ Personificar Utilizador apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -3834,7 +3834,7 @@ Apagar Utilizador apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -3942,7 +3942,7 @@ Atualizar saldo em Dinheiro apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3998,7 +3998,7 @@ Essa atividade já existe. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4078,7 +4078,7 @@ Meses libs/ui/src/lib/i18n.ts - 24 + 23 @@ -4086,7 +4086,7 @@ Anos libs/ui/src/lib/i18n.ts - 33 + 32 @@ -4094,7 +4094,7 @@ Mês libs/ui/src/lib/i18n.ts - 23 + 22 @@ -4102,7 +4102,7 @@ Ano libs/ui/src/lib/i18n.ts - 32 + 31 @@ -4110,7 +4110,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4122,7 +4122,7 @@ Responsabilidades apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -4262,7 +4262,7 @@ Responsabilidade libs/ui/src/lib/i18n.ts - 41 + 40 @@ -4506,7 +4506,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -4514,7 +4514,7 @@ De valor libs/ui/src/lib/i18n.ts - 43 + 42 @@ -4538,7 +4538,7 @@ Ativos apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -4546,7 +4546,7 @@ Predefinição libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4570,7 +4570,7 @@ Japão libs/ui/src/lib/i18n.ts - 93 + 92 @@ -4594,7 +4594,7 @@ Configurar as suas contas apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -4602,7 +4602,7 @@ Obtenha uma visão financeira abrangente adicionando as suas contas bancárias e de corretagem. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -4610,7 +4610,7 @@ Capture suas atividades apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -4618,7 +4618,7 @@ Registe as suas actividades de investimento para manter a sua carteira actualizada. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -4626,7 +4626,7 @@ Monitorizar e analisar a sua carteira apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -4634,7 +4634,7 @@ Acompanhe o seu progresso em tempo real com análises e conhecimentos abrangentes. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -4650,7 +4650,7 @@ Configurar contas apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -5380,7 +5380,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5392,7 +5392,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -5484,7 +5484,7 @@ Taxa apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5492,7 +5492,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5716,7 +5716,7 @@ Medo Extremo libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5724,7 +5724,7 @@ Ganância Extrema libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5732,7 +5732,7 @@ Neutro libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5748,7 +5748,7 @@ Você realmente deseja excluir esta mensagem do sistema? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5864,7 +5864,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5888,7 +5888,7 @@ Dados de mercado estão atrasados para apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5896,7 +5896,7 @@ Investimento apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5928,7 +5928,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5968,7 +5968,7 @@ Semana até agora libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5980,7 +5980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5988,7 +5988,7 @@ Do mês até a data libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6000,7 +6000,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6008,7 +6008,7 @@ No acumulado do ano libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6056,7 +6056,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6068,7 +6068,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6091,12 +6091,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General geral apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6104,7 +6112,7 @@ Nuvem apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6116,7 +6124,7 @@ Auto-hospedagem apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6157,7 +6165,7 @@ Ativo apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6165,7 +6173,7 @@ Fechado apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6173,7 +6181,7 @@ Indonésia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6181,7 +6189,7 @@ Atividade apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6189,7 +6197,7 @@ Rendimento de dividendos apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6221,7 +6229,7 @@ Liquidez libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6264,6 +6272,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Zona de perigo @@ -6285,7 +6301,7 @@ Por ETF Holding apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6293,7 +6309,7 @@ Aproximação baseada nas principais participações de cada ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6325,7 +6341,7 @@ Mostrar mais libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Austrália libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Áustria libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ Bélgica libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bulgária libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Canadá libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ República Tcheca libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finlândia libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ França libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Alemanha libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ Índia libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ Itália libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Holanda libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ Nova Zelândia libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Polônia libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Romênia libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ África do Sul libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Tailândia libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ Estados Unidos libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Inativo apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Sim libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Limite mínimo apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Limite máximo apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ Tenha acesso a mais de 80’000 tickers de mais de 50 bolsas libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Ucrânia libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Por favor, insira a sua chave da API do Ghostfolio: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ Pedidos de API Hoje apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ British Virgin Islands libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Generate Security Token apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ United Kingdom libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Do you really want to delete this item? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Log out apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo user account has been synced. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Investimento Alternativo libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Colecionável libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 26b2cb5bc..faae6b7e3 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -343,7 +343,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -399,7 +399,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -419,15 +419,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -539,7 +539,7 @@ Bu hesabı silmeyi gerçekten istiyor musunuz? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -559,11 +559,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -674,12 +674,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date Tarih apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -743,7 +751,7 @@ Filtrele... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -759,7 +767,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -783,7 +791,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -831,7 +839,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -843,11 +851,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -867,7 +875,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -887,7 +895,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -927,7 +935,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -955,7 +963,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -963,7 +971,7 @@ Bu kuponu gerçekten silmek istiyor musunuz? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -971,7 +979,7 @@ Önbelleği temizlemeyi gerçekten istiyor musunuz? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -979,7 +987,7 @@ Lütfen sistem mesajınızı belirleyin: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -987,7 +995,7 @@ Kullanıcı başına apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1211,7 +1219,7 @@ Günlük etkileşim apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1223,7 +1231,7 @@ Son Talep apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1231,7 +1239,7 @@ Kullanıcıyı Taklit Et apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1239,7 +1247,7 @@ Kullanıcıyı Sil apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1283,7 +1291,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1319,7 +1327,7 @@ Ghostfolio Hakkında apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1331,11 +1339,11 @@ Giriş apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1355,7 +1363,7 @@ Hay Allah! Güvenlik anahtarı yanlış. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1387,7 +1395,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1403,7 +1411,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1447,7 +1455,7 @@ Hesaplarınızı kurun apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -1455,7 +1463,7 @@ Banka ve yatırım hesaplarınızı ekleyerek kapsamlı finansal durumunuzu görün. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -1463,7 +1471,7 @@ İşlemlerinizi kaydedin apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -1471,7 +1479,7 @@ Yatırım işlemlerinizi kaydederek portföyünüzü güncel tutun. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -1479,7 +1487,7 @@ Portföyünüzü izleyin ve analiz edin. apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -1487,7 +1495,7 @@ Kapsamlı analiz ve içgörülerle ilerleme durumunuzu gerçek zamanlı olarak takip edin. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -1495,7 +1503,7 @@ Hesaplarınızı kurunuz apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1511,7 +1519,7 @@ İşlem ekle. apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1523,7 +1531,7 @@ Toplam Tutar apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1539,7 +1547,7 @@ Tasarruf Oranı apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1683,7 +1691,7 @@ Varlıklar apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -1691,7 +1699,7 @@ Alım Limiti apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1699,7 +1707,7 @@ Analize Dahil Edilmemiştir. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1707,7 +1715,7 @@ Yükümlülükler apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -1719,7 +1727,7 @@ Toplam Varlık apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1727,7 +1735,7 @@ Yıllıklandırılmış Performans apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1735,7 +1743,7 @@ Lütfen acil durum yedeği meblağını giriniz: apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1743,7 +1751,7 @@ Asgari Fiyat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1751,7 +1759,7 @@ Azami Fiyat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1759,11 +1767,11 @@ Miktar apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1779,7 +1787,7 @@ Komisyon apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1971,7 +1979,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1983,7 +1991,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1995,7 +2003,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2007,7 +2015,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2027,7 +2035,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2051,7 +2059,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2063,7 +2071,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2079,7 +2087,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2095,7 +2103,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2195,7 +2203,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2203,11 +2211,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2259,7 +2267,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -2271,7 +2279,7 @@ Piyasa Verileri apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -2323,7 +2331,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2331,11 +2339,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2523,7 +2531,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -2655,7 +2663,7 @@ Başla apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -2735,7 +2743,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -2751,7 +2759,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -3159,15 +3167,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3235,7 +3243,7 @@ Nakit Bakiyesini Güncelle apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3243,7 +3251,7 @@ Birim Fiyat apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3255,7 +3263,7 @@ İşlemleri İçe Aktar apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3271,7 +3279,7 @@ Temettüleri İçe Aktar apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3287,7 +3295,7 @@ Veri içe aktarılıyor... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -3295,7 +3303,7 @@ İçe aktarma tamamlandı apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -3311,7 +3319,7 @@ Veri doğrulanıyor... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3399,7 +3407,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -3503,7 +3511,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -3603,11 +3611,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3615,7 +3623,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3623,7 +3631,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3743,11 +3751,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -3987,7 +3995,7 @@ Kayıt apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -4300,7 +4308,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -4312,7 +4320,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -4328,7 +4336,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4360,7 +4368,7 @@ Benim Ghostfolio’m apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -4732,7 +4740,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -4784,7 +4792,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -4792,7 +4800,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -4836,7 +4844,7 @@ Hesap apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4876,11 +4884,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -4908,11 +4916,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -4943,14 +4951,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Ghostfolio Açık Kaynak veya Ghostfolio Temel’e kolayca geçin. - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund Acil Durum Fonu @@ -4964,7 +4964,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -4972,7 +4972,7 @@ Hibe libs/ui/src/lib/i18n.ts - 19 + 18 @@ -4980,7 +4980,7 @@ Daha Yüksek Risk libs/ui/src/lib/i18n.ts - 20 + 19 @@ -4988,7 +4988,7 @@ Bu işlem zaten mevcut. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -4996,7 +4996,7 @@ Japonya libs/ui/src/lib/i18n.ts - 93 + 92 @@ -5004,7 +5004,7 @@ Daha Düşük Risk libs/ui/src/lib/i18n.ts - 22 + 21 @@ -5012,7 +5012,7 @@ Ay libs/ui/src/lib/i18n.ts - 23 + 22 @@ -5020,7 +5020,7 @@ Ay libs/ui/src/lib/i18n.ts - 24 + 23 @@ -5028,7 +5028,7 @@ Diğer libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -5040,7 +5040,7 @@ Önceden Ayarlanmış libs/ui/src/lib/i18n.ts - 27 + 26 @@ -5056,7 +5056,7 @@ Yaşlılık Provizyonu libs/ui/src/lib/i18n.ts - 28 + 27 @@ -5072,7 +5072,7 @@ Uydu libs/ui/src/lib/i18n.ts - 29 + 28 @@ -5096,11 +5096,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -5108,7 +5108,7 @@ Etiket libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -5120,7 +5120,7 @@ Yıl libs/ui/src/lib/i18n.ts - 32 + 31 @@ -5128,7 +5128,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -5140,7 +5140,7 @@ Yıl libs/ui/src/lib/i18n.ts - 33 + 32 @@ -5160,7 +5160,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -5168,7 +5168,7 @@ Kıymet libs/ui/src/lib/i18n.ts - 43 + 42 @@ -5176,7 +5176,7 @@ Yükümlülük libs/ui/src/lib/i18n.ts - 41 + 40 @@ -5188,7 +5188,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -5196,11 +5196,11 @@ Nakit apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -5208,7 +5208,7 @@ Emtia libs/ui/src/lib/i18n.ts - 47 + 46 @@ -5220,7 +5220,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -5228,7 +5228,7 @@ Sabit Gelir libs/ui/src/lib/i18n.ts - 49 + 48 @@ -5236,7 +5236,7 @@ Gayrimenkul libs/ui/src/lib/i18n.ts - 51 + 50 @@ -5252,7 +5252,7 @@ Bono libs/ui/src/lib/i18n.ts - 54 + 53 @@ -5260,7 +5260,7 @@ Kriptopara libs/ui/src/lib/i18n.ts - 57 + 56 @@ -5268,7 +5268,7 @@ Borsada İşlem Gören Fonlar (ETF) libs/ui/src/lib/i18n.ts - 58 + 57 @@ -5276,7 +5276,7 @@ Borsada İşlem Görmeyen Fonlar (Mutual Fund) libs/ui/src/lib/i18n.ts - 60 + 59 @@ -5284,7 +5284,7 @@ Kıymetli Metaller libs/ui/src/lib/i18n.ts - 61 + 60 @@ -5292,7 +5292,7 @@ Özel Menkul Kıymetler libs/ui/src/lib/i18n.ts - 62 + 61 @@ -5300,7 +5300,7 @@ Hisse Senetleri libs/ui/src/lib/i18n.ts - 63 + 62 @@ -5308,7 +5308,7 @@ Afrika libs/ui/src/lib/i18n.ts - 70 + 69 @@ -5316,7 +5316,7 @@ Asya libs/ui/src/lib/i18n.ts - 71 + 70 @@ -5324,7 +5324,7 @@ Avrupa libs/ui/src/lib/i18n.ts - 72 + 71 @@ -5332,7 +5332,7 @@ Kuzey Amerika libs/ui/src/lib/i18n.ts - 73 + 72 @@ -5348,7 +5348,7 @@ Okyanusya libs/ui/src/lib/i18n.ts - 74 + 73 @@ -5356,7 +5356,7 @@ Güney Amerika libs/ui/src/lib/i18n.ts - 75 + 74 @@ -5392,7 +5392,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -5492,7 +5492,7 @@ Ücret apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5500,7 +5500,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5716,7 +5716,7 @@ Aşırı Korku libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5724,7 +5724,7 @@ Aşırı Açgözlülük libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5732,7 +5732,7 @@ Nötr libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5748,7 +5748,7 @@ Bu sistem mesajını silmeyi gerçekten istiyor musunuz? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -5864,7 +5864,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5888,7 +5888,7 @@ Piyasa verileri gecikmeli apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5896,7 +5896,7 @@ Yatırım apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5928,7 +5928,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5968,7 +5968,7 @@ Hafta içi libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5980,7 +5980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5988,7 +5988,7 @@ Ay içi libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6000,7 +6000,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6008,7 +6008,7 @@ Yıl içi libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6056,7 +6056,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6068,7 +6068,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6091,12 +6091,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General Genel apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6104,7 +6112,7 @@ Bulut apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6116,7 +6124,7 @@ Kendini Barındırma apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6157,7 +6165,7 @@ Aktif apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6165,7 +6173,7 @@ Kapalı apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6173,7 +6181,7 @@ Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6181,7 +6189,7 @@ Etkinlik apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6189,7 +6197,7 @@ Temettü Getiri apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6221,7 +6229,7 @@ Likidite libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6264,6 +6272,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone Tehlikeli Alan @@ -6285,7 +6301,7 @@ ETF Portföyü apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6293,7 +6309,7 @@ Her ETF’nin en üst tutarlarına dayalı yaklaşım apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6325,7 +6341,7 @@ Daha fazla göster libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6561,7 +6577,7 @@ Avustralya libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6569,7 +6585,7 @@ Avusturya libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6577,7 +6593,7 @@ Belçika libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6585,7 +6601,7 @@ Bulgaristan libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6601,7 +6617,7 @@ Kanada libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6609,7 +6625,7 @@ Çek Cumhuriyeti libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6617,7 +6633,7 @@ Finlandiya libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6625,7 +6641,7 @@ Fransa libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6633,7 +6649,7 @@ Almanya libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6641,7 +6657,7 @@ Hindistan libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6649,7 +6665,7 @@ İtalya libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6657,7 +6673,7 @@ Hollanda libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6665,7 +6681,7 @@ Yeni Zelanda libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6673,7 +6689,7 @@ Polonya libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6681,7 +6697,7 @@ Romanya libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6689,7 +6705,7 @@ Güney Afrika libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6697,7 +6713,7 @@ Tayland libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6705,7 +6721,7 @@ Amerika Birleşik Devletleri libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6753,7 +6769,7 @@ Pasif apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6797,7 +6813,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6837,7 +6853,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6853,7 +6869,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6873,7 +6889,7 @@ Evet libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6921,7 +6937,7 @@ Eşik Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6929,7 +6945,7 @@ Eşik Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6961,7 +6977,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7141,7 +7157,7 @@ 80’000+ sembolden 50’den fazla borsada erişim alın libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7149,7 +7165,7 @@ Ukraine libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7169,7 +7185,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7185,7 +7201,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7271,7 +7287,7 @@ Ghostfolio API anahtarınızı girin: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7279,7 +7295,7 @@ API Günü İstekleri apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7375,11 +7391,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7391,7 +7407,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Ermenistan libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ Britanya Virjin Adaları libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapur libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Güvenlik belirteci oluştur apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ Birleşik Krallık libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ birisi apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Bu öğeyi silmek istediğinize emin misiniz? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Oturumu kapat apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo kullanıcı hesabı senkronize edildi. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Alternative Investment libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Collectible libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 43da07020..edc28f604 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -10,7 +10,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -34,11 +34,11 @@ Увійти apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -471,7 +471,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -527,7 +527,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -547,15 +547,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -667,7 +667,7 @@ Ви дійсно хочете видалити цей обліковий запис? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -739,11 +739,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -879,7 +879,7 @@ Фільтрувати за... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -911,7 +911,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -935,7 +935,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -1031,7 +1031,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -1043,11 +1043,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1067,7 +1067,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -1087,7 +1087,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1163,7 +1163,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -1215,7 +1215,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1231,7 +1231,7 @@ Ви дійсно хочете видалити цей купон? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -1239,7 +1239,7 @@ Ви дійсно хочете видалити це системне повідомлення? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -1247,7 +1247,7 @@ Ви дійсно хочете очистити кеш? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -1255,7 +1255,7 @@ Будь ласка, встановіть ваше системне повідомлення: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1271,7 +1271,7 @@ на користувача apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1302,6 +1302,14 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + System Message Системне повідомлення @@ -1615,7 +1623,7 @@ Взаємодія за день apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1627,7 +1635,7 @@ Запити API сьогодні apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1639,7 +1647,7 @@ Останній запит apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1647,7 +1655,7 @@ Видавати себе за користувача apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1655,7 +1663,7 @@ Видалити користувача apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1699,7 +1707,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1779,7 +1787,7 @@ Про Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1791,7 +1799,7 @@ Упс! Неправильний Секретний Токен. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1831,7 +1839,7 @@ Мінімальна ціна apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1839,7 +1847,7 @@ Максимальна ціна apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1847,11 +1855,11 @@ Кількість apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1867,7 +1875,7 @@ Дохідність дивіденду apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -1875,7 +1883,7 @@ Комісії apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1887,7 +1895,7 @@ Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 @@ -1895,7 +1903,7 @@ Активність apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -1911,7 +1919,7 @@ Активний apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -1919,7 +1927,7 @@ Закритий apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -1959,7 +1967,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1975,7 +1983,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -2019,7 +2027,7 @@ Налаштуйте ваші рахунки apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -2027,7 +2035,7 @@ Отримайте комплексний фінансовий огляд, додавши ваші банківські та брокерські рахунки. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -2035,7 +2043,7 @@ Фіксуйте свою діяльність apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -2043,7 +2051,7 @@ Записуйте ваші інвестиційні дії, щоб підтримувати актуальність вашого портфеля. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -2051,7 +2059,7 @@ Відстежуйте та аналізуйте свій портфель apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -2059,7 +2067,7 @@ Відстежуйте свій прогрес в режимі реального часу за допомогою всебічного аналізу та інсайтів apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -2067,7 +2075,7 @@ Налаштувати рахунки apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -2083,7 +2091,7 @@ Додати активність apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -2095,7 +2103,7 @@ Загальна сума apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -2111,7 +2119,7 @@ Ставка заощаджень apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -2171,7 +2179,7 @@ Ринкові дані затримуються для apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -2235,7 +2243,7 @@ Активи apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -2243,7 +2251,7 @@ Купівельна спроможність apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -2251,7 +2259,7 @@ Виключено з аналізу apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -2259,7 +2267,7 @@ Зобов’язання apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -2271,7 +2279,7 @@ Чиста вартість apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -2279,7 +2287,7 @@ Річна доходність apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -2307,11 +2315,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -2323,7 +2331,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -2335,7 +2343,7 @@ Будь ласка, встановіть суму вашого резервного фонду. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -2351,7 +2359,7 @@ Мінімальний поріг apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -2359,7 +2367,7 @@ Максимальний поріг apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -2559,7 +2567,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -2571,7 +2579,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -2583,7 +2591,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2595,7 +2603,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2615,7 +2623,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2631,7 +2639,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -2731,7 +2739,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2846,6 +2854,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Do you really want to remove this sign in method? Ви дійсно хочете вилучити цей спосіб входу? @@ -3059,7 +3075,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -3079,7 +3095,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -3103,7 +3119,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -3203,7 +3219,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -3211,11 +3227,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3299,7 +3315,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -3319,7 +3335,7 @@ Ринкові дані apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -3371,7 +3387,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -3379,11 +3395,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -3399,7 +3415,7 @@ Будь ласка, введіть ваш ключ API Ghostfolio: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -3575,7 +3591,7 @@ Загальні apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -3583,7 +3599,7 @@ Хмара apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -3595,7 +3611,7 @@ Самохостинг apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -3724,7 +3740,7 @@ Почати apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -3804,7 +3820,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -3820,7 +3836,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -4320,15 +4336,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -4412,7 +4428,15 @@ Оновити баланс готівки apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 + + + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 @@ -4420,7 +4444,7 @@ Дата apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -4440,7 +4464,7 @@ Ціна за одиницю apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4452,7 +4476,7 @@ Імпортувати активності apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4468,7 +4492,7 @@ Імпорт дивідендів apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4484,7 +4508,7 @@ Імпортуються дані... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -4492,7 +4516,7 @@ Імпорт завершено apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -4508,7 +4532,7 @@ Перевірка даних... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -4604,7 +4628,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -4708,7 +4732,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -4784,7 +4808,7 @@ За активами ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -4792,7 +4816,7 @@ Наближення на основі провідних активів кожного ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -4824,11 +4848,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -4836,7 +4860,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -4844,7 +4868,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -4852,7 +4876,7 @@ Інвестиції apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -4900,7 +4924,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5044,7 +5068,7 @@ Неактивний apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -5060,11 +5084,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -5272,7 +5296,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -5324,7 +5348,7 @@ Реєстрація apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -5360,7 +5384,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -5376,7 +5400,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -5475,7 +5499,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -5487,7 +5511,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -6035,7 +6059,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -6083,7 +6107,7 @@ Мій Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -6199,7 +6223,7 @@ Тиждень до дати libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6211,7 +6235,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6219,7 +6243,7 @@ Місяць до дати libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6231,7 +6255,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6239,7 +6263,7 @@ Рік до дати libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -6259,7 +6283,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6271,7 +6295,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6371,7 +6395,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -6439,7 +6463,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -6447,7 +6471,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -6507,7 +6531,7 @@ Рахунок apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -6547,11 +6571,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -6579,11 +6603,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -6639,7 +6663,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6687,7 +6711,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6703,7 +6727,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6726,14 +6750,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Переключитися на Ghostfolio Open Source або Ghostfolio Basic легко - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund Резервний фонд @@ -6747,7 +6763,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -6755,7 +6771,7 @@ Грант libs/ui/src/lib/i18n.ts - 19 + 18 @@ -6763,7 +6779,7 @@ Вищий ризик libs/ui/src/lib/i18n.ts - 20 + 19 @@ -6771,7 +6787,7 @@ Така активність вже існує. libs/ui/src/lib/i18n.ts - 21 + 20 @@ -6779,7 +6795,7 @@ Нижчий ризик libs/ui/src/lib/i18n.ts - 22 + 21 @@ -6787,7 +6803,7 @@ Місяць libs/ui/src/lib/i18n.ts - 23 + 22 @@ -6795,7 +6811,7 @@ Місяців libs/ui/src/lib/i18n.ts - 24 + 23 @@ -6803,7 +6819,7 @@ Інші libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -6815,7 +6831,7 @@ Отримайте доступ до 80 000+ тікерів з понад 50 бірж libs/ui/src/lib/i18n.ts - 26 + 25 @@ -6823,7 +6839,7 @@ Пресет libs/ui/src/lib/i18n.ts - 27 + 26 @@ -6839,7 +6855,7 @@ Пенсійне накопичення libs/ui/src/lib/i18n.ts - 28 + 27 @@ -6855,7 +6871,7 @@ Супутник libs/ui/src/lib/i18n.ts - 29 + 28 @@ -6879,11 +6895,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -6891,7 +6907,7 @@ Тег libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -6903,7 +6919,7 @@ Рік libs/ui/src/lib/i18n.ts - 32 + 31 @@ -6911,7 +6927,7 @@ View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -6923,7 +6939,7 @@ Роки libs/ui/src/lib/i18n.ts - 33 + 32 @@ -6939,7 +6955,7 @@ Так libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6959,7 +6975,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -6967,7 +6983,7 @@ Комісія apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -6975,7 +6991,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -6983,7 +6999,7 @@ Цінний libs/ui/src/lib/i18n.ts - 43 + 42 @@ -6991,7 +7007,7 @@ Зобов’язання libs/ui/src/lib/i18n.ts - 41 + 40 @@ -7003,7 +7019,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -7011,11 +7027,11 @@ Готівка apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -7023,7 +7039,7 @@ Товар libs/ui/src/lib/i18n.ts - 47 + 46 @@ -7035,7 +7051,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -7043,7 +7059,7 @@ Фіксований дохід libs/ui/src/lib/i18n.ts - 49 + 48 @@ -7051,7 +7067,7 @@ Ліквідність libs/ui/src/lib/i18n.ts - 50 + 49 @@ -7059,7 +7075,7 @@ Нерухомість libs/ui/src/lib/i18n.ts - 51 + 50 @@ -7075,7 +7091,7 @@ Облігація libs/ui/src/lib/i18n.ts - 54 + 53 @@ -7083,7 +7099,7 @@ Криптовалюта libs/ui/src/lib/i18n.ts - 57 + 56 @@ -7091,7 +7107,7 @@ ETF libs/ui/src/lib/i18n.ts - 58 + 57 @@ -7099,7 +7115,7 @@ Взаємний фонд libs/ui/src/lib/i18n.ts - 60 + 59 @@ -7107,7 +7123,7 @@ Дорогоцінний метал libs/ui/src/lib/i18n.ts - 61 + 60 @@ -7115,7 +7131,7 @@ Приватний капітал libs/ui/src/lib/i18n.ts - 62 + 61 @@ -7123,7 +7139,7 @@ Акція libs/ui/src/lib/i18n.ts - 63 + 62 @@ -7131,7 +7147,7 @@ Африка libs/ui/src/lib/i18n.ts - 70 + 69 @@ -7139,7 +7155,7 @@ Азія libs/ui/src/lib/i18n.ts - 71 + 70 @@ -7147,7 +7163,7 @@ Європа libs/ui/src/lib/i18n.ts - 72 + 71 @@ -7155,7 +7171,7 @@ Північна Америка libs/ui/src/lib/i18n.ts - 73 + 72 @@ -7171,7 +7187,7 @@ Океанія libs/ui/src/lib/i18n.ts - 74 + 73 @@ -7179,7 +7195,7 @@ Південна Америка libs/ui/src/lib/i18n.ts - 75 + 74 @@ -7187,7 +7203,7 @@ Австралія libs/ui/src/lib/i18n.ts - 80 + 79 @@ -7195,7 +7211,7 @@ Австрія libs/ui/src/lib/i18n.ts - 81 + 80 @@ -7203,7 +7219,7 @@ Бельгія libs/ui/src/lib/i18n.ts - 82 + 81 @@ -7211,7 +7227,7 @@ Болгарія libs/ui/src/lib/i18n.ts - 84 + 83 @@ -7227,7 +7243,7 @@ Канада libs/ui/src/lib/i18n.ts - 85 + 84 @@ -7235,7 +7251,7 @@ Чеська Республіка libs/ui/src/lib/i18n.ts - 86 + 85 @@ -7243,7 +7259,7 @@ Фінляндія libs/ui/src/lib/i18n.ts - 87 + 86 @@ -7251,7 +7267,7 @@ Франція libs/ui/src/lib/i18n.ts - 88 + 87 @@ -7259,7 +7275,7 @@ Німеччина libs/ui/src/lib/i18n.ts - 89 + 88 @@ -7267,7 +7283,7 @@ Індія libs/ui/src/lib/i18n.ts - 90 + 89 @@ -7275,7 +7291,7 @@ Італія libs/ui/src/lib/i18n.ts - 92 + 91 @@ -7283,7 +7299,7 @@ Японія libs/ui/src/lib/i18n.ts - 93 + 92 @@ -7291,7 +7307,7 @@ Нідерланди libs/ui/src/lib/i18n.ts - 94 + 93 @@ -7299,7 +7315,7 @@ Нова Зеландія libs/ui/src/lib/i18n.ts - 95 + 94 @@ -7307,7 +7323,7 @@ Польща libs/ui/src/lib/i18n.ts - 96 + 95 @@ -7315,7 +7331,7 @@ Румунія libs/ui/src/lib/i18n.ts - 97 + 96 @@ -7323,7 +7339,7 @@ Південна Африка libs/ui/src/lib/i18n.ts - 99 + 98 @@ -7331,7 +7347,7 @@ Таїланд libs/ui/src/lib/i18n.ts - 101 + 100 @@ -7339,7 +7355,7 @@ Україна libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7347,7 +7363,7 @@ Сполучені Штати libs/ui/src/lib/i18n.ts - 104 + 103 @@ -7355,7 +7371,7 @@ Екстремальний страх libs/ui/src/lib/i18n.ts - 107 + 106 @@ -7363,7 +7379,7 @@ Екстремальна жадібність libs/ui/src/lib/i18n.ts - 108 + 107 @@ -7371,7 +7387,7 @@ Нейтрально libs/ui/src/lib/i18n.ts - 111 + 110 @@ -7423,7 +7439,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -7439,7 +7455,7 @@ Показати більше libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -7547,7 +7563,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7559,7 +7575,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7567,11 +7583,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7611,7 +7627,7 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7619,7 +7635,7 @@ British Virgin Islands libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7627,7 +7643,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7690,20 +7706,12 @@ 240 - - Find account, holding or page... - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token Generate Security Token apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7711,7 +7719,7 @@ United Kingdom libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7832,7 +7840,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7864,7 +7872,7 @@ Do you really want to delete this item? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7872,7 +7880,7 @@ Log out apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7905,7 +7913,7 @@ Demo user account has been synced. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8332,7 +8340,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8348,7 +8356,7 @@ Alternative Investment libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8356,7 +8364,7 @@ Collectible libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index a9a4b563d..4c38d4f7d 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -366,7 +366,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -420,7 +420,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -439,15 +439,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -556,7 +556,7 @@ Do you really want to delete this account? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -593,11 +593,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -674,11 +674,18 @@ 2 + + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -744,7 +751,7 @@ Filter by... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -759,7 +766,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -781,7 +788,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -856,7 +863,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -867,7 +874,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -878,11 +885,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -901,7 +908,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -920,7 +927,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -956,7 +963,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -996,35 +1003,35 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 Do you really want to delete this coupon? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 Do you really want to delete this system message? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 Do you really want to flush the cache? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 Please set your system message: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1038,7 +1045,7 @@ per User apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1266,7 +1273,7 @@ Engagement per Day apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1277,21 +1284,21 @@ Last Request apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 Impersonate User apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 Delete User apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1331,7 +1338,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1364,7 +1371,7 @@ About Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1375,11 +1382,11 @@ Sign in apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1398,7 +1405,7 @@ Oops! Incorrect Security Token. apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1428,7 +1435,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1443,7 +1450,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1482,49 +1489,49 @@ Setup your accounts apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 Get a comprehensive financial overview by adding your bank and brokerage accounts. apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 Capture your activities apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 Record your investment activities to keep your portfolio up to date. apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 Monitor and analyze your portfolio apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 Track your progress in real-time with comprehensive analysis and insights. apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 Setup accounts apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1538,7 +1545,7 @@ Add activity apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1549,7 +1556,7 @@ Total Amount apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1563,7 +1570,7 @@ Savings Rate apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1668,7 +1675,7 @@ Fees apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1708,28 +1715,28 @@ Assets apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 Buying Power apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 Excluded from Analysis apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 Liabilities apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -1740,46 +1747,46 @@ Net Worth apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 Annualized Performance apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 Please set the amount of your emergency fund. apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 Minimum Price apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 Maximum Price apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 Quantity apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -1962,7 +1969,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -1973,7 +1980,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -1984,7 +1991,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -1995,7 +2002,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2013,7 +2020,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2277,7 +2284,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2288,7 +2295,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2303,7 +2310,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2318,7 +2325,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2410,7 +2417,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2418,11 +2425,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2498,7 +2505,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -2509,7 +2516,7 @@ Market Data apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -2558,7 +2565,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2566,11 +2573,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2751,7 +2758,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -2857,7 +2864,7 @@ Get Started apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -2934,7 +2941,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -2950,7 +2957,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -3387,15 +3394,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3478,14 +3485,14 @@ Update Cash Balance apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 Unit Price apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3496,7 +3503,7 @@ Import Activities apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3511,7 +3518,7 @@ Import Dividends apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3526,14 +3533,14 @@ Importing data... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 Import has been completed apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -3547,7 +3554,7 @@ Validating data... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -3711,7 +3718,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -3801,11 +3808,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -3813,7 +3820,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -3821,7 +3828,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -3927,11 +3934,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -4147,7 +4154,7 @@ Registration apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -4442,7 +4449,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -4453,7 +4460,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -4468,7 +4475,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4512,7 +4519,7 @@ My Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -4661,7 +4668,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -4707,7 +4714,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -4715,7 +4722,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -4755,7 +4762,7 @@ Account apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -4793,11 +4800,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -4824,11 +4831,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -4856,13 +4863,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund @@ -4875,63 +4875,63 @@ libs/ui/src/lib/i18n.ts - 16 + 15 Grant libs/ui/src/lib/i18n.ts - 19 + 18 Higher Risk libs/ui/src/lib/i18n.ts - 20 + 19 This activity already exists. libs/ui/src/lib/i18n.ts - 21 + 20 Japan libs/ui/src/lib/i18n.ts - 93 + 92 Lower Risk libs/ui/src/lib/i18n.ts - 22 + 21 Month libs/ui/src/lib/i18n.ts - 23 + 22 Months libs/ui/src/lib/i18n.ts - 24 + 23 Other libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -4942,7 +4942,7 @@ Preset libs/ui/src/lib/i18n.ts - 27 + 26 @@ -4956,14 +4956,14 @@ Retirement Provision libs/ui/src/lib/i18n.ts - 28 + 27 Satellite libs/ui/src/lib/i18n.ts - 29 + 28 @@ -4986,18 +4986,18 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 Tag libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -5008,14 +5008,14 @@ Year libs/ui/src/lib/i18n.ts - 32 + 31 View Details apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -5026,7 +5026,7 @@ Years libs/ui/src/lib/i18n.ts - 33 + 32 @@ -5044,14 +5044,14 @@ libs/ui/src/lib/i18n.ts - 37 + 36 Fee apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5059,21 +5059,21 @@ libs/ui/src/lib/i18n.ts - 39 + 38 Valuable libs/ui/src/lib/i18n.ts - 43 + 42 Liability libs/ui/src/lib/i18n.ts - 41 + 40 @@ -5084,25 +5084,25 @@ libs/ui/src/lib/i18n.ts - 42 + 41 Cash apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 Commodity libs/ui/src/lib/i18n.ts - 47 + 46 @@ -5113,21 +5113,21 @@ libs/ui/src/lib/i18n.ts - 48 + 47 Fixed Income libs/ui/src/lib/i18n.ts - 49 + 48 Real Estate libs/ui/src/lib/i18n.ts - 51 + 50 @@ -5141,77 +5141,77 @@ Bond libs/ui/src/lib/i18n.ts - 54 + 53 Cryptocurrency libs/ui/src/lib/i18n.ts - 57 + 56 ETF libs/ui/src/lib/i18n.ts - 58 + 57 Mutual Fund libs/ui/src/lib/i18n.ts - 60 + 59 Precious Metal libs/ui/src/lib/i18n.ts - 61 + 60 Private Equity libs/ui/src/lib/i18n.ts - 62 + 61 Stock libs/ui/src/lib/i18n.ts - 63 + 62 Africa libs/ui/src/lib/i18n.ts - 70 + 69 Asia libs/ui/src/lib/i18n.ts - 71 + 70 Europe libs/ui/src/lib/i18n.ts - 72 + 71 North America libs/ui/src/lib/i18n.ts - 73 + 72 @@ -5225,35 +5225,35 @@ Oceania libs/ui/src/lib/i18n.ts - 74 + 73 South America libs/ui/src/lib/i18n.ts - 75 + 74 Extreme Fear libs/ui/src/lib/i18n.ts - 107 + 106 Extreme Greed libs/ui/src/lib/i18n.ts - 108 + 107 Neutral libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5298,7 +5298,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -5340,7 +5340,7 @@ Argentina libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5383,7 +5383,7 @@ Market data is delayed for apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5397,7 +5397,7 @@ Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5411,7 +5411,7 @@ Investment apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5455,21 +5455,21 @@ Year to date libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 Week to date libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 Month to date libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -5480,7 +5480,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -5491,7 +5491,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5535,7 +5535,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -5546,7 +5546,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -5572,7 +5572,7 @@ Self-Hosting apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -5590,18 +5590,25 @@ 76 + + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 Cloud apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -5626,35 +5633,35 @@ Closed apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 Active apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 Indonesia libs/ui/src/lib/i18n.ts - 91 + 90 Activity apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 Dividend Yield apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -5682,7 +5689,7 @@ Liquidity libs/ui/src/lib/i18n.ts - 50 + 49 @@ -5727,6 +5734,13 @@ 205 + + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone @@ -5738,14 +5752,14 @@ Approximation based on the top holdings of each ETF apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 By ETF Holding apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -5773,7 +5787,7 @@ Show more libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -5895,35 +5909,35 @@ Thailand libs/ui/src/lib/i18n.ts - 101 + 100 India libs/ui/src/lib/i18n.ts - 90 + 89 Austria libs/ui/src/lib/i18n.ts - 81 + 80 Poland libs/ui/src/lib/i18n.ts - 96 + 95 Italy libs/ui/src/lib/i18n.ts - 92 + 91 @@ -5972,21 +5986,21 @@ Canada libs/ui/src/lib/i18n.ts - 85 + 84 New Zealand libs/ui/src/lib/i18n.ts - 95 + 94 Netherlands libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6025,21 +6039,21 @@ Romania libs/ui/src/lib/i18n.ts - 97 + 96 Germany libs/ui/src/lib/i18n.ts - 89 + 88 United States libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6053,7 +6067,7 @@ Belgium libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6071,28 +6085,28 @@ Czech Republic libs/ui/src/lib/i18n.ts - 86 + 85 Australia libs/ui/src/lib/i18n.ts - 80 + 79 South Africa libs/ui/src/lib/i18n.ts - 99 + 98 Bulgaria libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6106,14 +6120,14 @@ Finland libs/ui/src/lib/i18n.ts - 87 + 86 France libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6163,7 +6177,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6189,7 +6203,7 @@ Yes libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6203,7 +6217,7 @@ Inactive apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6230,7 +6244,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6246,7 +6260,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6278,7 +6292,7 @@ Threshold Max apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6306,7 +6320,7 @@ Threshold Min apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6355,7 +6369,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -6496,7 +6510,7 @@ Ukraine libs/ui/src/lib/i18n.ts - 102 + 101 @@ -6510,7 +6524,7 @@ Get access to 80’000+ tickers from over 50 exchanges libs/ui/src/lib/i18n.ts - 26 + 25 @@ -6535,7 +6549,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -6562,7 +6576,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -6599,7 +6613,7 @@ Please enter your Ghostfolio API key: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -6648,7 +6662,7 @@ API Requests Today apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -6721,11 +6735,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6737,7 +6751,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -6875,7 +6889,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -6886,7 +6900,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -6894,11 +6908,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -6919,7 +6933,7 @@ Singapore libs/ui/src/lib/i18n.ts - 98 + 97 @@ -6933,14 +6947,14 @@ Armenia libs/ui/src/lib/i18n.ts - 78 + 77 British Virgin Islands libs/ui/src/lib/i18n.ts - 83 + 82 @@ -6992,13 +7006,6 @@ 240 - - Find account, holding or page... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Security token @@ -7014,14 +7021,14 @@ Generate Security Token apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 United Kingdom libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7107,7 +7114,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7136,14 +7143,14 @@ Do you really want to delete this item? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 Log out apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7180,7 +7187,7 @@ Demo user account has been synced. apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -7552,7 +7559,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -7566,14 +7573,14 @@ Alternative Investment libs/ui/src/lib/i18n.ts - 46 + 45 Collectible libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 9224b8ace..991b5f8b3 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -392,7 +392,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 135 + 134 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -448,7 +448,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 141 + 140 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -468,15 +468,15 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 201 + 199 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 204 + 202 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 207 + 205 libs/ui/src/lib/account-balances/account-balances.component.html @@ -588,7 +588,7 @@ 您确定要删除此账户吗? libs/ui/src/lib/accounts-table/accounts-table.component.ts - 148 + 146 @@ -628,11 +628,11 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 151 + 150 libs/ui/src/lib/i18n.ts - 15 + 14 @@ -719,12 +719,20 @@ 2 + + Find an account... + Find an account... + + libs/ui/src/lib/assistant/assistant.component.ts + 471 + + Date 日期 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 157 + 156 libs/ui/src/lib/account-balances/account-balances.component.html @@ -796,7 +804,7 @@ 过滤... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts - 383 + 368 @@ -812,7 +820,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 219 + 217 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -836,7 +844,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 40 + 42 @@ -896,7 +904,7 @@ libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html - 67 + 69 @@ -908,7 +916,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 268 + 266 @@ -920,11 +928,11 @@ apps/client/src/app/components/admin-users/admin-users.html - 60 + 56 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 278 + 276 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -944,7 +952,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 284 + 282 apps/client/src/app/pages/public/public-page.html @@ -964,7 +972,7 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 296 + 294 @@ -1004,7 +1012,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 271 + 268 @@ -1048,7 +1056,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 120 + 119 @@ -1056,7 +1064,7 @@ 您确实要删除此优惠券吗? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 221 + 222 @@ -1064,7 +1072,7 @@ 您真的要删除这条系统消息吗? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 234 + 235 @@ -1072,7 +1080,7 @@ 您真的要刷新缓存吗? apps/client/src/app/components/admin-overview/admin-overview.component.ts - 258 + 259 @@ -1080,7 +1088,7 @@ 请设置您的系统消息: apps/client/src/app/components/admin-overview/admin-overview.component.ts - 278 + 279 @@ -1096,7 +1104,7 @@ 每位用户 apps/client/src/app/components/admin-overview/admin-overview.component.ts - 165 + 166 @@ -1352,7 +1360,7 @@ 每天的参与度 apps/client/src/app/components/admin-users/admin-users.html - 140 + 136 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1364,7 +1372,7 @@ 最后请求 apps/client/src/app/components/admin-users/admin-users.html - 186 + 182 @@ -1372,7 +1380,7 @@ 模拟用户 apps/client/src/app/components/admin-users/admin-users.html - 233 + 229 @@ -1380,7 +1388,7 @@ 删除用户 apps/client/src/app/components/admin-users/admin-users.html - 254 + 250 @@ -1424,7 +1432,7 @@ apps/client/src/app/components/header/header.component.html - 258 + 257 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -1460,7 +1468,7 @@ 关于 Ghostfolio apps/client/src/app/components/header/header.component.html - 326 + 322 apps/client/src/app/pages/about/overview/about-overview-page.html @@ -1472,11 +1480,11 @@ 登入 apps/client/src/app/components/header/header.component.html - 425 + 421 apps/client/src/app/components/header/header.component.ts - 298 + 296 apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html @@ -1496,7 +1504,7 @@ 哎呀!安全令牌不正确。 apps/client/src/app/components/header/header.component.ts - 313 + 311 apps/client/src/app/components/user-account-access/user-account-access.component.ts @@ -1528,7 +1536,7 @@ libs/ui/src/lib/i18n.ts - 109 + 108 @@ -1544,7 +1552,7 @@ libs/ui/src/lib/i18n.ts - 110 + 109 @@ -1588,7 +1596,7 @@ 设置您的帐户 apps/client/src/app/components/home-overview/home-overview.html - 19 + 16 @@ -1596,7 +1604,7 @@ 通过添加您的银行和经纪账户来获取全面的财务概览。 apps/client/src/app/components/home-overview/home-overview.html - 21 + 18 @@ -1604,7 +1612,7 @@ 记录你的活动 apps/client/src/app/components/home-overview/home-overview.html - 28 + 25 @@ -1612,7 +1620,7 @@ 记录您的投资活动以使您的投资组合保持最新状态。 apps/client/src/app/components/home-overview/home-overview.html - 30 + 27 @@ -1620,7 +1628,7 @@ 监控和分析您的投资组合 apps/client/src/app/components/home-overview/home-overview.html - 37 + 34 @@ -1628,7 +1636,7 @@ 通过全面的分析和见解实时跟踪您的进度。 apps/client/src/app/components/home-overview/home-overview.html - 39 + 36 @@ -1636,7 +1644,7 @@ 设置帐户 apps/client/src/app/components/home-overview/home-overview.html - 52 + 49 @@ -1652,7 +1660,7 @@ 添加活动 apps/client/src/app/components/home-overview/home-overview.html - 60 + 57 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -1664,7 +1672,7 @@ 总金额 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 146 + 147 @@ -1680,7 +1688,7 @@ 储蓄率 apps/client/src/app/components/investment-chart/investment-chart.component.ts - 202 + 205 @@ -1792,7 +1800,7 @@ 费用 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 208 + 206 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -1836,7 +1844,7 @@ 资产 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 233 + 232 @@ -1844,7 +1852,7 @@ 购买力 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 248 + 247 @@ -1852,7 +1860,7 @@ 从分析中排除 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 274 + 273 @@ -1860,7 +1868,7 @@ 负债 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 302 + 301 apps/client/src/app/pages/features/features-page.html @@ -1872,7 +1880,7 @@ 净值 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 324 + 323 @@ -1880,7 +1888,7 @@ 年化业绩 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 338 + 337 @@ -1888,7 +1896,7 @@ 请输入您的应急基金金额。 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 111 + 110 @@ -1896,7 +1904,7 @@ 最低价格 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 130 + 129 @@ -1904,7 +1912,7 @@ 最高价格 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 147 + 145 @@ -1912,11 +1920,11 @@ 数量 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 157 + 155 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 185 + 183 libs/ui/src/lib/activities-table/activities-table.component.html @@ -2112,7 +2120,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 367 + 373 @@ -2124,7 +2132,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -2136,7 +2144,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -2148,7 +2156,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -2168,7 +2176,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 419 + 425 @@ -2464,7 +2472,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 194 + 191 @@ -2476,7 +2484,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 192 + 189 @@ -2492,7 +2500,7 @@ apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 195 + 192 @@ -2508,7 +2516,7 @@ apps/client/src/app/components/header/header.component.html - 374 + 370 apps/client/src/app/pages/about/overview/about-overview-page.routes.ts @@ -2608,7 +2616,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 97 + 93 apps/client/src/app/components/header/header.component.html @@ -2616,11 +2624,11 @@ apps/client/src/app/components/header/header.component.html - 268 + 267 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 377 + 378 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -2704,7 +2712,7 @@ apps/client/src/app/components/header/header.component.html - 289 + 287 libs/common/src/lib/routes/routes.ts @@ -2716,7 +2724,7 @@ 市场数据 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 399 + 400 libs/common/src/lib/routes/routes.ts @@ -2768,7 +2776,7 @@ apps/client/src/app/components/header/header.component.html - 248 + 247 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -2776,11 +2784,11 @@ apps/client/src/app/pages/admin/admin-page.component.ts - 45 + 41 apps/client/src/app/pages/resources/resources-page.component.ts - 29 + 27 libs/common/src/lib/routes/routes.ts @@ -2968,7 +2976,7 @@ apps/client/src/app/components/header/header.component.html - 360 + 356 apps/client/src/app/pages/features/features-page.html @@ -3088,7 +3096,7 @@ 立即开始 apps/client/src/app/components/header/header.component.html - 436 + 432 apps/client/src/app/pages/features/features-page.html @@ -3168,7 +3176,7 @@ apps/client/src/app/components/header/header.component.html - 407 + 403 apps/client/src/app/components/home-market/home-market.html @@ -3184,7 +3192,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 39 + 37 libs/common/src/lib/routes/routes.ts @@ -3676,15 +3684,15 @@ apps/client/src/app/components/admin-users/admin-users.html - 118 + 114 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 231 + 229 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 344 + 342 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -3776,7 +3784,7 @@ 更新现金余额 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 108 + 109 @@ -3784,7 +3792,7 @@ 单价 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 210 + 208 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3796,7 +3804,7 @@ 导入活动记录 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 92 + 93 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3812,7 +3820,7 @@ 导入股息 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 136 + 132 libs/ui/src/lib/activities-table/activities-table.component.html @@ -3828,7 +3836,7 @@ 正在导入数据... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 174 + 171 @@ -3836,7 +3844,7 @@ 导入已完成 apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 184 + 181 @@ -3852,7 +3860,7 @@ 验证数据... apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts - 293 + 291 @@ -4036,7 +4044,7 @@ libs/ui/src/lib/i18n.ts - 17 + 16 @@ -4136,11 +4144,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 186 + 184 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 372 + 371 apps/client/src/app/pages/features/features-page.html @@ -4148,7 +4156,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 198 + 196 apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -4156,7 +4164,7 @@ libs/ui/src/lib/i18n.ts - 38 + 37 @@ -4276,11 +4284,11 @@ apps/client/src/app/components/header/header.component.html - 313 + 309 apps/client/src/app/components/header/header.component.html - 388 + 384 apps/client/src/app/pages/pricing/pricing-page.routes.ts @@ -4520,7 +4528,7 @@ 注册 apps/client/src/app/components/admin-users/admin-users.html - 80 + 76 libs/common/src/lib/routes/routes.ts @@ -4845,7 +4853,7 @@ libs/ui/src/lib/i18n.ts - 100 + 99 @@ -4857,7 +4865,7 @@ libs/ui/src/lib/i18n.ts - 18 + 17 @@ -4873,7 +4881,7 @@ apps/client/src/app/components/header/header.component.html - 301 + 297 apps/client/src/app/pages/resources/overview/resources-overview.component.html @@ -4921,7 +4929,7 @@ 我的 Ghostfolio apps/client/src/app/components/header/header.component.html - 277 + 276 apps/client/src/app/pages/user-account/user-account-page.routes.ts @@ -5089,7 +5097,7 @@ Loan libs/ui/src/lib/i18n.ts - 59 + 58 @@ -5141,7 +5149,7 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 359 + 358 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -5149,7 +5157,7 @@ libs/ui/src/lib/i18n.ts - 40 + 39 @@ -5193,7 +5201,7 @@ 帐户 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 85 + 86 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5233,11 +5241,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 242 + 240 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 286 + 283 libs/ui/src/lib/i18n.ts @@ -5265,11 +5273,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 251 + 249 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 305 + 302 libs/ui/src/lib/i18n.ts @@ -5300,14 +5308,6 @@ 13 - - Switch to Ghostfolio Open Source or Ghostfolio Basic easily - 轻松切换到 Ghostfolio Open Source 或 Ghostfolio Basic - - libs/ui/src/lib/i18n.ts - 14 - - Emergency Fund 应急基金 @@ -5321,7 +5321,7 @@ libs/ui/src/lib/i18n.ts - 16 + 15 @@ -5329,7 +5329,7 @@ 授予 libs/ui/src/lib/i18n.ts - 19 + 18 @@ -5337,7 +5337,7 @@ 风险较高 libs/ui/src/lib/i18n.ts - 20 + 19 @@ -5345,7 +5345,7 @@ 这项活动已经存在。 libs/ui/src/lib/i18n.ts - 21 + 20 @@ -5353,7 +5353,7 @@ 日本 libs/ui/src/lib/i18n.ts - 93 + 92 @@ -5361,7 +5361,7 @@ 降低风险 libs/ui/src/lib/i18n.ts - 22 + 21 @@ -5369,7 +5369,7 @@ libs/ui/src/lib/i18n.ts - 23 + 22 @@ -5377,7 +5377,7 @@ 几个月 libs/ui/src/lib/i18n.ts - 24 + 23 @@ -5385,7 +5385,7 @@ 其他 libs/ui/src/lib/i18n.ts - 25 + 24 libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -5397,7 +5397,7 @@ 预设 libs/ui/src/lib/i18n.ts - 27 + 26 @@ -5413,7 +5413,7 @@ 退休金 libs/ui/src/lib/i18n.ts - 28 + 27 @@ -5421,7 +5421,7 @@ 卫星 libs/ui/src/lib/i18n.ts - 29 + 28 @@ -5445,11 +5445,11 @@ apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 316 + 314 libs/ui/src/lib/i18n.ts - 30 + 29 @@ -5457,7 +5457,7 @@ 标签 libs/ui/src/lib/i18n.ts - 31 + 30 libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -5469,7 +5469,7 @@ libs/ui/src/lib/i18n.ts - 32 + 31 @@ -5477,7 +5477,7 @@ 查看详细信息 apps/client/src/app/components/admin-users/admin-users.html - 225 + 221 libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -5489,7 +5489,7 @@ libs/ui/src/lib/i18n.ts - 33 + 32 @@ -5509,7 +5509,7 @@ libs/ui/src/lib/i18n.ts - 37 + 36 @@ -5517,7 +5517,7 @@ 费用 apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 258 + 255 libs/ui/src/lib/activities-table/activities-table.component.html @@ -5525,7 +5525,7 @@ libs/ui/src/lib/i18n.ts - 39 + 38 @@ -5533,7 +5533,7 @@ 贵重物品 libs/ui/src/lib/i18n.ts - 43 + 42 @@ -5541,7 +5541,7 @@ 负债 libs/ui/src/lib/i18n.ts - 41 + 40 @@ -5553,7 +5553,7 @@ libs/ui/src/lib/i18n.ts - 42 + 41 @@ -5561,11 +5561,11 @@ 现金 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 219 + 218 libs/ui/src/lib/i18n.ts - 55 + 54 @@ -5573,7 +5573,7 @@ 商品 libs/ui/src/lib/i18n.ts - 47 + 46 @@ -5585,7 +5585,7 @@ libs/ui/src/lib/i18n.ts - 48 + 47 @@ -5593,7 +5593,7 @@ 固定收益 libs/ui/src/lib/i18n.ts - 49 + 48 @@ -5601,7 +5601,7 @@ 房地产 libs/ui/src/lib/i18n.ts - 51 + 50 @@ -5617,7 +5617,7 @@ 债券 libs/ui/src/lib/i18n.ts - 54 + 53 @@ -5625,7 +5625,7 @@ 加密货币 libs/ui/src/lib/i18n.ts - 57 + 56 @@ -5633,7 +5633,7 @@ 交易所交易基金 libs/ui/src/lib/i18n.ts - 58 + 57 @@ -5641,7 +5641,7 @@ 共同基金 libs/ui/src/lib/i18n.ts - 60 + 59 @@ -5649,7 +5649,7 @@ 贵金属 libs/ui/src/lib/i18n.ts - 61 + 60 @@ -5657,7 +5657,7 @@ 私募股权 libs/ui/src/lib/i18n.ts - 62 + 61 @@ -5665,7 +5665,7 @@ 股票 libs/ui/src/lib/i18n.ts - 63 + 62 @@ -5673,7 +5673,7 @@ 非洲 libs/ui/src/lib/i18n.ts - 70 + 69 @@ -5681,7 +5681,7 @@ 亚洲 libs/ui/src/lib/i18n.ts - 71 + 70 @@ -5689,7 +5689,7 @@ 欧洲 libs/ui/src/lib/i18n.ts - 72 + 71 @@ -5697,7 +5697,7 @@ 北美 libs/ui/src/lib/i18n.ts - 73 + 72 @@ -5713,7 +5713,7 @@ 大洋洲 libs/ui/src/lib/i18n.ts - 74 + 73 @@ -5721,7 +5721,7 @@ 南美洲 libs/ui/src/lib/i18n.ts - 75 + 74 @@ -5729,7 +5729,7 @@ 极度恐惧 libs/ui/src/lib/i18n.ts - 107 + 106 @@ -5737,7 +5737,7 @@ 极度贪婪 libs/ui/src/lib/i18n.ts - 108 + 107 @@ -5745,7 +5745,7 @@ 中性的 libs/ui/src/lib/i18n.ts - 111 + 110 @@ -5793,7 +5793,7 @@ libs/ui/src/lib/top-holdings/top-holdings.component.html - 181 + 186 @@ -5841,7 +5841,7 @@ 阿根廷 libs/ui/src/lib/i18n.ts - 79 + 78 @@ -5889,7 +5889,7 @@ 市场数据延迟 apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts - 95 + 94 @@ -5905,7 +5905,7 @@ 关闭持仓 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 443 + 442 @@ -5921,7 +5921,7 @@ 投资 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 171 + 169 apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -5969,7 +5969,7 @@ 今年迄今为止 libs/ui/src/lib/assistant/assistant.component.ts - 379 + 385 @@ -5977,7 +5977,7 @@ 本周至今 libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -5985,7 +5985,7 @@ 本月至今 libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -5997,7 +5997,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 375 + 381 @@ -6009,7 +6009,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 371 + 377 @@ -6057,7 +6057,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 389 + 395 @@ -6069,7 +6069,7 @@ libs/ui/src/lib/assistant/assistant.component.ts - 413 + 419 @@ -6098,7 +6098,7 @@ 自托管 apps/client/src/app/pages/faq/faq-page.component.ts - 52 + 46 libs/common/src/lib/routes/routes.ts @@ -6117,12 +6117,20 @@ 76 + + Find a holding... + Find a holding... + + libs/ui/src/lib/assistant/assistant.component.ts + 472 + + General 一般的 apps/client/src/app/pages/faq/faq-page.component.ts - 41 + 35 @@ -6130,7 +6138,7 @@ apps/client/src/app/pages/faq/faq-page.component.ts - 46 + 40 libs/common/src/lib/routes/routes.ts @@ -6158,7 +6166,7 @@ 已关闭 apps/client/src/app/components/home-holdings/home-holdings.component.ts - 64 + 62 @@ -6166,7 +6174,7 @@ 活跃 apps/client/src/app/components/home-holdings/home-holdings.component.ts - 63 + 61 @@ -6174,7 +6182,7 @@ 印度尼西亚 libs/ui/src/lib/i18n.ts - 91 + 90 @@ -6182,7 +6190,7 @@ 活动 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 229 + 227 @@ -6190,7 +6198,7 @@ 股息收益率 apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html - 196 + 194 @@ -6222,7 +6230,7 @@ 流动性 libs/ui/src/lib/i18n.ts - 50 + 49 @@ -6265,6 +6273,14 @@ 205 + + Jump to a page... + Jump to a page... + + libs/ui/src/lib/assistant/assistant.component.ts + 473 + + Danger Zone 危险区域 @@ -6286,7 +6302,7 @@ 按 ETF 持仓 apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 329 + 327 @@ -6294,7 +6310,7 @@ 基于每个 ETF 的主要持仓的近似值 apps/client/src/app/pages/portfolio/allocations/allocations-page.html - 336 + 334 @@ -6326,7 +6342,7 @@ 显示更多 libs/ui/src/lib/top-holdings/top-holdings.component.html - 174 + 179 @@ -6562,7 +6578,7 @@ 澳大利亚 libs/ui/src/lib/i18n.ts - 80 + 79 @@ -6570,7 +6586,7 @@ 奥地利 libs/ui/src/lib/i18n.ts - 81 + 80 @@ -6578,7 +6594,7 @@ 比利时 libs/ui/src/lib/i18n.ts - 82 + 81 @@ -6586,7 +6602,7 @@ 保加利亚 libs/ui/src/lib/i18n.ts - 84 + 83 @@ -6602,7 +6618,7 @@ 加拿大 libs/ui/src/lib/i18n.ts - 85 + 84 @@ -6610,7 +6626,7 @@ 捷克共和国 libs/ui/src/lib/i18n.ts - 86 + 85 @@ -6618,7 +6634,7 @@ 芬兰 libs/ui/src/lib/i18n.ts - 87 + 86 @@ -6626,7 +6642,7 @@ 法国 libs/ui/src/lib/i18n.ts - 88 + 87 @@ -6634,7 +6650,7 @@ 德国 libs/ui/src/lib/i18n.ts - 89 + 88 @@ -6642,7 +6658,7 @@ 印度 libs/ui/src/lib/i18n.ts - 90 + 89 @@ -6650,7 +6666,7 @@ 意大利 libs/ui/src/lib/i18n.ts - 92 + 91 @@ -6658,7 +6674,7 @@ 荷兰 libs/ui/src/lib/i18n.ts - 94 + 93 @@ -6666,7 +6682,7 @@ 新西兰 libs/ui/src/lib/i18n.ts - 95 + 94 @@ -6674,7 +6690,7 @@ 波兰 libs/ui/src/lib/i18n.ts - 96 + 95 @@ -6682,7 +6698,7 @@ 罗马尼亚 libs/ui/src/lib/i18n.ts - 97 + 96 @@ -6690,7 +6706,7 @@ 南非 libs/ui/src/lib/i18n.ts - 99 + 98 @@ -6698,7 +6714,7 @@ 泰国 libs/ui/src/lib/i18n.ts - 101 + 100 @@ -6706,7 +6722,7 @@ 美国 libs/ui/src/lib/i18n.ts - 104 + 103 @@ -6754,7 +6770,7 @@ 非活跃 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html - 88 + 85 @@ -6798,7 +6814,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 341 + 338 apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -6838,7 +6854,7 @@ apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 130 + 124 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -6854,7 +6870,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 343 + 340 libs/ui/src/lib/i18n.ts @@ -6874,7 +6890,7 @@ libs/ui/src/lib/i18n.ts - 34 + 33 @@ -6922,7 +6938,7 @@ 最小阈值 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 55 + 52 @@ -6930,7 +6946,7 @@ 阈值上限 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 93 + 87 @@ -6962,7 +6978,7 @@ has been copied to the clipboard apps/client/src/app/components/admin-overview/admin-overview.component.ts - 377 + 378 libs/ui/src/lib/value/value.component.ts @@ -7142,7 +7158,7 @@ 获取来自 50 多个交易所的 80,000+ 股票代码访问权限 libs/ui/src/lib/i18n.ts - 26 + 25 @@ -7150,7 +7166,7 @@ 乌克兰 libs/ui/src/lib/i18n.ts - 102 + 101 @@ -7170,7 +7186,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 44 + 42 libs/common/src/lib/routes/routes.ts @@ -7186,7 +7202,7 @@ apps/client/src/app/pages/resources/resources-page.component.ts - 33 + 31 libs/common/src/lib/routes/routes.ts @@ -7272,7 +7288,7 @@ 请输入您的 Ghostfolio API 密钥: apps/client/src/app/pages/api/api-page.component.ts - 46 + 62 @@ -7280,7 +7296,7 @@ 今日 API 请求数 apps/client/src/app/components/admin-users/admin-users.html - 161 + 157 apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -7376,11 +7392,11 @@ apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts - 109 + 108 apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html - 136 + 130 apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -7392,7 +7408,7 @@ apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html - 352 + 349 libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -7548,7 +7564,7 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 @@ -7560,7 +7576,7 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 54 + 52 libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -7568,11 +7584,11 @@ libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 377 + 381 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 390 + 394 @@ -7612,7 +7628,7 @@ 亚美尼亚 libs/ui/src/lib/i18n.ts - 78 + 77 @@ -7620,7 +7636,7 @@ 英属维尔京群岛 libs/ui/src/lib/i18n.ts - 83 + 82 @@ -7628,7 +7644,7 @@ 新加坡 libs/ui/src/lib/i18n.ts - 98 + 97 @@ -7691,20 +7707,12 @@ 240 - - Find account, holding or page... - 查找账户、持仓或页面... - - libs/ui/src/lib/assistant/assistant.component.ts - 116 - - Generate Security Token 生成安全令牌 apps/client/src/app/components/admin-users/admin-users.html - 243 + 239 @@ -7712,7 +7720,7 @@ 英国 libs/ui/src/lib/i18n.ts - 103 + 102 @@ -7833,7 +7841,7 @@ 某人 apps/client/src/app/pages/public/public-page.component.ts - 64 + 62 @@ -7865,7 +7873,7 @@ 您确定要删除此项目吗? libs/ui/src/lib/benchmark/benchmark.component.ts - 139 + 137 @@ -7873,7 +7881,7 @@ 登出 apps/client/src/app/components/header/header.component.html - 329 + 325 @@ -7906,7 +7914,7 @@ 演示用户账户已同步。 apps/client/src/app/components/admin-overview/admin-overview.component.ts - 302 + 303 @@ -8333,7 +8341,7 @@ apps/client/src/app/components/admin-users/admin-users.html - 39 + 35 @@ -8349,7 +8357,7 @@ 另类投资 libs/ui/src/lib/i18n.ts - 46 + 45 @@ -8357,7 +8365,7 @@ 收藏品 libs/ui/src/lib/i18n.ts - 56 + 55 diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 0b7f6f029..1eb5bd2dd 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -265,16 +265,6 @@ body { } } - .page { - &.has-tabs { - @media (min-width: 576px) { - .mat-mdc-tab-header { - background-color: rgba(var(--palette-foreground-base-dark), 0.02); - } - } - } - } - .svgMap-tooltip { background: var(--dark-background); @@ -375,7 +365,8 @@ ngx-skeleton-loader { } .has-info-message { - .page.has-tabs { + // Restrict viewport height of tabbed views when the Live Demo or system announcements banner are displayed + .page:has(gf-page-tabs) { height: calc(100svh - 2 * var(--mat-toolbar-standard-height)); } } @@ -489,7 +480,6 @@ ngx-skeleton-loader { .page { display: flex; - flex-direction: column; overflow-y: auto; padding-bottom: env(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom); @@ -501,47 +491,15 @@ ngx-skeleton-loader { z-index: 999; } - &:not(.has-tabs) { - @media (min-width: 576px) { - padding: 2rem 0; - } - } - - &.has-tabs { + // Restrict viewport height and layout boundaries only when the page hosts tab navigation + &:has(gf-page-tabs) { height: calc(100svh - var(--mat-toolbar-standard-height)); + } - .fab-container { - @media (max-width: 575.98px) { - bottom: 5rem; - } - } - - .mat-mdc-tab-nav-panel { - padding: 2rem 0; - } - + // Apply vertical padding only to standalone or nested content views (tabs handle their own padding) + &:not(:has(gf-page-tabs)) { @media (min-width: 576px) { - flex-direction: row-reverse; - - @include mat.tabs-overrides( - ( - divider-height: 0 - ) - ); - - .mat-mdc-tab-header { - background-color: rgba(var(--palette-foreground-base), 0.02); - padding: 2rem 0; - width: 14rem; - - .mat-mdc-tab-links { - flex-direction: column; - - .mat-mdc-tab-link { - justify-content: flex-start; - } - } - } + padding: 2rem 0; } } } diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index 296270e7f..f8a194651 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -150,7 +150,9 @@ $gf-typography: ( ); .theme-light { - $gf-theme-default: mat.define-theme( + color-scheme: light; + + @include mat.theme( ( color: ( primary: $_primary, @@ -164,8 +166,6 @@ $gf-typography: ( ) ); - @include mat.all-component-themes($gf-theme-default); - @include mat.autocomplete-overrides( ( background-color: var(--light-background) @@ -233,7 +233,9 @@ $gf-typography: ( } .theme-dark { - $gf-theme-dark: mat.define-theme( + color-scheme: dark; + + @include mat.theme( ( color: ( primary: $_primary, @@ -247,8 +249,6 @@ $gf-typography: ( ) ); - @include mat.all-component-themes($gf-theme-dark); - @include mat.button-overrides( ( outlined-label-text-color: var(--light-primary-text), @@ -293,24 +293,6 @@ $gf-typography: ( container-shape: 4px ) ); - - .page.has-tabs { - @include mat.tabs-overrides( - ( - container-height: 3rem - ) - ); - } - } - - @media (min-width: 576px) { - .page.has-tabs { - @include mat.tabs-overrides( - ( - container-height: 2rem - ) - ); - } } @include mat.badge-overrides( @@ -436,15 +418,6 @@ $gf-typography: ( ) ); } - - .page.has-tabs { - @include mat.tabs-overrides( - ( - active-indicator-height: 0, - label-text-tracking: normal - ) - ); - } } :root { diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 42e1f6b63..113dffe4a 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -2,6 +2,8 @@ import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; import { JobOptions, JobStatus } from 'bull'; import ms from 'ms'; +import { ColorScheme, DateRange } from './types'; + export const ghostfolioPrefix = 'GF'; export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = @@ -77,8 +79,10 @@ export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW = export const STATISTICS_GATHERING_QUEUE = 'STATISTICS_GATHERING_QUEUE'; +export const DEFAULT_COLOR_SCHEME: ColorScheme = 'LIGHT'; export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; +export const DEFAULT_DATE_RANGE: DateRange = 'max'; export const DEFAULT_HOST = '0.0.0.0'; export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_PAGE_SIZE = 50; diff --git a/libs/common/src/lib/dtos/create-asset-profile.dto.ts b/libs/common/src/lib/dtos/create-asset-profile.dto.ts index 85ad73cc0..1bd3ba9a8 100644 --- a/libs/common/src/lib/dtos/create-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile.dto.ts @@ -74,7 +74,7 @@ export class CreateAssetProfileDto { @IsOptional() @IsUrl({ - protocols: ['https'], + protocols: ['http', 'https'], require_protocol: true }) url?: string; diff --git a/libs/common/src/lib/dtos/create-platform.dto.ts b/libs/common/src/lib/dtos/create-platform.dto.ts index 941354c11..e482dd241 100644 --- a/libs/common/src/lib/dtos/create-platform.dto.ts +++ b/libs/common/src/lib/dtos/create-platform.dto.ts @@ -5,7 +5,7 @@ export class CreatePlatformDto { name: string; @IsUrl({ - protocols: ['https'], + protocols: ['http', 'https'], require_protocol: true }) url: string; diff --git a/libs/common/src/lib/dtos/update-asset-profile.dto.ts b/libs/common/src/lib/dtos/update-asset-profile.dto.ts index 43f5aa617..a4981493e 100644 --- a/libs/common/src/lib/dtos/update-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/update-asset-profile.dto.ts @@ -64,7 +64,7 @@ export class UpdateAssetProfileDto { @IsOptional() @IsUrl({ - protocols: ['https'], + protocols: ['http', 'https'], require_protocol: true }) url?: string; diff --git a/libs/common/src/lib/dtos/update-platform.dto.ts b/libs/common/src/lib/dtos/update-platform.dto.ts index 4c4f907af..03ae94b21 100644 --- a/libs/common/src/lib/dtos/update-platform.dto.ts +++ b/libs/common/src/lib/dtos/update-platform.dto.ts @@ -8,7 +8,7 @@ export class UpdatePlatformDto { name: string; @IsUrl({ - protocols: ['https'], + protocols: ['http', 'https'], require_protocol: true }) url: string; diff --git a/libs/common/src/lib/dtos/update-property.dto.ts b/libs/common/src/lib/dtos/update-property.dto.ts index 77115759a..befb26ab2 100644 --- a/libs/common/src/lib/dtos/update-property.dto.ts +++ b/libs/common/src/lib/dtos/update-property.dto.ts @@ -3,5 +3,5 @@ import { IsOptional, IsString } from 'class-validator'; export class UpdatePropertyDto { @IsOptional() @IsString() - value: string; + value?: string; } diff --git a/libs/common/src/lib/dtos/update-user-setting.dto.ts b/libs/common/src/lib/dtos/update-user-setting.dto.ts index f2781835c..d46982e70 100644 --- a/libs/common/src/lib/dtos/update-user-setting.dto.ts +++ b/libs/common/src/lib/dtos/update-user-setting.dto.ts @@ -58,23 +58,23 @@ export class UpdateUserSettingDto { @IsArray() @IsOptional() - 'filters.accounts'?: string[]; + 'filters.accounts'?: string[] | null; @IsArray() @IsOptional() - 'filters.assetClasses'?: string[]; + 'filters.assetClasses'?: string[] | null; @IsString() @IsOptional() - 'filters.dataSource'?: string; + 'filters.dataSource'?: string | null; @IsString() @IsOptional() - 'filters.symbol'?: string; + 'filters.symbol'?: string | null; @IsArray() @IsOptional() - 'filters.tags'?: string[]; + 'filters.tags'?: string[] | null; @IsIn(['CHART', 'TABLE'] as HoldingsViewMode[]) @IsOptional() diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 4db1fcf2d..cbaf657dd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -187,7 +187,7 @@ export function getCurrencyFromSymbol(aSymbol = '') { return aSymbol.replace(DEFAULT_CURRENCY, ''); } -export function getDateFnsLocale(aLanguageCode: string) { +export function getDateFnsLocale(aLanguageCode?: string) { if (aLanguageCode === 'ca') { return ca; } else if (aLanguageCode === 'de') { @@ -342,20 +342,6 @@ export function getYesterday() { return subDays(new Date(Date.UTC(year, month, day)), 1); } -export function groupBy( - key: K, - arr: T[] -): Map { - const map = new Map(); - arr.forEach((t) => { - if (!map.has(t[key])) { - map.set(t[key], []); - } - map.get(t[key])!.push(t); - }); - return map; -} - export function interpolate(template: string, context: any) { return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => { const properties = objectPath.split('.'); diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index ad747d94e..536fc0feb 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -44,6 +44,7 @@ import type { ActivityResponse } from './responses/activity-response.interface'; import type { AdminUserResponse } from './responses/admin-user-response.interface'; import type { AdminUsersResponse } from './responses/admin-users-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; +import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { AssetResponse } from './responses/asset-response.interface'; import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface'; @@ -90,7 +91,6 @@ import type { SubscriptionOffer } from './subscription-offer.interface'; import type { SymbolItem } from './symbol-item.interface'; import type { SymbolMetrics } from './symbol-metrics.interface'; import type { SystemMessage } from './system-message.interface'; -import type { TabConfiguration } from './tab-configuration.interface'; import type { ToggleOption } from './toggle-option.interface'; import type { UserItem } from './user-item.interface'; import type { UserSettings } from './user-settings.interface'; @@ -117,6 +117,7 @@ export { AdminUserResponse, AdminUsersResponse, AiPromptResponse, + AiServiceHealthResponse, ApiKeyResponse, AssertionCredentialJSON, AssetClassSelectorOption, @@ -184,7 +185,6 @@ export { SymbolItem, SymbolMetrics, SystemMessage, - TabConfiguration, ToggleOption, User, UserItem, diff --git a/libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts b/libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts new file mode 100644 index 000000000..58ffafafc --- /dev/null +++ b/libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts @@ -0,0 +1,3 @@ +export interface AiServiceHealthResponse { + status: string; +} diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index eae14cec6..18c7dc57a 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -14,32 +14,10 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { [symbol: string]: Pick< PortfolioPosition, | 'allocationInPercentage' - - /** @deprecated */ - | 'assetClass' | 'assetProfile' - - /** @deprecated */ - | 'countries' - | 'currency' - - /** @deprecated */ - | 'dataSource' | 'dateOfFirstActivity' | 'markets' - - /** @deprecated */ - | 'name' | 'netPerformancePercentWithCurrencyEffect' - - /** @deprecated */ - | 'sectors' - - /** @deprecated */ - | 'symbol' - - /** @deprecated */ - | 'url' | 'valueInBaseCurrency' | 'valueInPercentage' >; diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 15f5bb21f..1ba0ecc56 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -340,15 +340,13 @@
    diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index aa104f795..1526ffea5 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -4,7 +4,6 @@ import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfValueComponent } from '@ghostfolio/ui/value'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -37,7 +36,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, GfEntityLogoComponent, GfValueComponent, IonIcon, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 5a4ae641c..ae5cf0384 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -516,9 +516,7 @@ @@ -536,11 +534,9 @@ } (null); public dateRangeOptions: DateRangeOption[] = []; public holdings: PortfolioPosition[] = []; + public isLoading = { accounts: false, assetProfiles: false, holdings: false, quickLinks: false }; + public isOpen = false; - public placeholder = $localize`Find account, holding or page...`; + public placeholder: string; + public portfolioFilterFormControl = new FormControl( { account: null, @@ -122,13 +125,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { tag: null } ); + public searchFormControl = new FormControl(''); + public searchResults: SearchResults = { accounts: [], assetProfiles: [], holdings: [], quickLinks: [] }; + public tags: Filter[] = []; protected readonly closed = output(); @@ -458,7 +464,15 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { holdings: true, quickLinks: true }; + this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap(); + + this.placeholder = sample([ + $localize`Find an account...`, + $localize`Find a holding...`, + $localize`Jump to a page...` + ]); + this.searchResults = { accounts: [], assetProfiles: [], @@ -471,6 +485,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } this.searchFormControl.setValue(''); + setTimeout(() => { this.searchElement?.nativeElement?.focus(); }); @@ -481,6 +496,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { holdings: false, quickLinks: false }; + this.setIsOpen(true); this.dataService diff --git a/libs/ui/src/lib/benchmark/benchmark.component.html b/libs/ui/src/lib/benchmark/benchmark.component.html index 8820f2ec1..a950f0c44 100644 --- a/libs/ui/src/lib/benchmark/benchmark.component.html +++ b/libs/ui/src/lib/benchmark/benchmark.component.html @@ -7,11 +7,22 @@ matSortDirection="asc" [dataSource]="dataSource" > - - + + + + + + + + + Name - +
    {{ element?.name }}
    @@ -26,14 +37,14 @@ 50-Day Trend
    @@ -55,14 +66,14 @@ 200-Day Trend
    @@ -84,14 +95,14 @@ Last All Time High
    @@ -109,7 +120,7 @@ @@ -118,18 +129,18 @@ > from ATH - + @if (isNumber(element?.performances?.allTimeHigh?.performancePercent)) { } diff --git a/libs/ui/src/lib/benchmark/benchmark.component.ts b/libs/ui/src/lib/benchmark/benchmark.component.ts index 5dbc4648b..0b78ffa0d 100644 --- a/libs/ui/src/lib/benchmark/benchmark.component.ts +++ b/libs/ui/src/lib/benchmark/benchmark.component.ts @@ -11,7 +11,6 @@ import { } from '@ghostfolio/common/interfaces'; import { NotificationService } from '@ghostfolio/ui/notifications'; -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -37,6 +36,7 @@ import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; import { isNumber } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; import { translate } from '../i18n'; import { GfTrendIndicatorComponent } from '../trend-indicator/trend-indicator.component'; import { GfValueComponent } from '../value/value.component'; @@ -46,7 +46,7 @@ import { BenchmarkDetailDialogParams } from './benchmark-detail-dialog/interface @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, + GfEntityLogoComponent, GfTrendIndicatorComponent, GfValueComponent, IonIcon, @@ -67,6 +67,7 @@ export class GfBenchmarkComponent { public readonly deviceType = input.required(); public readonly hasPermissionToDeleteItem = input(); public readonly locale = input(getLocale()); + public readonly showIcon = input(false); public readonly showSymbol = input(true); public readonly user = input(); @@ -77,6 +78,7 @@ export class GfBenchmarkComponent { protected readonly dataSource = new MatTableDataSource([]); protected readonly displayedColumns = computed(() => { return [ + ...(this.showIcon() ? ['icon'] : []), 'name', ...(this.user()?.settings?.isExperimentalFeatures ? ['trend50d', 'trend200d'] diff --git a/libs/ui/src/lib/dialog-header/dialog-header.component.html b/libs/ui/src/lib/dialog-header/dialog-header.component.html index 019d85a52..ed9fc41f4 100644 --- a/libs/ui/src/lib/dialog-header/dialog-header.component.html +++ b/libs/ui/src/lib/dialog-header/dialog-header.component.html @@ -1,7 +1,7 @@
    {{ title }} @if (deviceType !== 'mobile') { diff --git a/libs/ui/src/lib/dialog-header/dialog-header.component.ts b/libs/ui/src/lib/dialog-header/dialog-header.component.ts index ce3173d0e..4868b22b8 100644 --- a/libs/ui/src/lib/dialog-header/dialog-header.component.ts +++ b/libs/ui/src/lib/dialog-header/dialog-header.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -15,7 +14,7 @@ import { close } from 'ionicons/icons'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'justify-content-center' }, - imports: [CommonModule, IonIcon, MatButtonModule, MatDialogModule], + imports: [IonIcon, MatButtonModule, MatDialogModule], selector: 'gf-dialog-header', styleUrls: ['./dialog-header.component.scss'], templateUrl: './dialog-header.component.html' diff --git a/libs/ui/src/lib/entity-logo/entity-logo.component.html b/libs/ui/src/lib/entity-logo/entity-logo.component.html index 942ea23e5..d8aeba136 100644 --- a/libs/ui/src/lib/entity-logo/entity-logo.component.html +++ b/libs/ui/src/lib/entity-logo/entity-logo.component.html @@ -1,7 +1,7 @@ @if (src) { diff --git a/libs/ui/src/lib/entity-logo/entity-logo.component.ts b/libs/ui/src/lib/entity-logo/entity-logo.component.ts index 212e232be..ba7d64ae0 100644 --- a/libs/ui/src/lib/entity-logo/entity-logo.component.ts +++ b/libs/ui/src/lib/entity-logo/entity-logo.component.ts @@ -1,6 +1,5 @@ import { EntityLogoImageSourceService } from '@ghostfolio/ui/entity-logo/entity-logo-image-source.service'; -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -12,7 +11,6 @@ import { DataSource } from '@prisma/client'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-entity-logo', styleUrls: ['./entity-logo.component.scss'], diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html index 91e3dd8d7..065e62de1 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -6,14 +6,16 @@ @for (day of days; track day) {
    - + Name
    - {{ element.name }} - @if (element.name === element.symbol) { + {{ element.assetProfile.name }} + @if (element.assetProfile.name === element.assetProfile.symbol) { ({{ element.assetProfile.assetSubClassLabel }}) }
    - {{ element.symbol }} + {{ element.assetProfile.symbol }}
    @@ -185,8 +190,8 @@ (click)=" canShowDetails(row) && onOpenHoldingDialog({ - dataSource: row.dataSource, - symbol: row.symbol + dataSource: row.assetProfile.dataSource, + symbol: row.assetProfile.symbol }) " > diff --git a/libs/ui/src/lib/logo/logo.component.html b/libs/ui/src/lib/logo/logo.component.html index 6e31e0f9e..a67da73bc 100644 --- a/libs/ui/src/lib/logo/logo.component.html +++ b/libs/ui/src/lib/logo/logo.component.html @@ -1,5 +1,5 @@ + > @if (showLabel) { {{ label ?? 'Ghostfolio' }} } diff --git a/libs/ui/src/lib/logo/logo.component.ts b/libs/ui/src/lib/logo/logo.component.ts index 0b766429c..a5f6a58e4 100644 --- a/libs/ui/src/lib/logo/logo.component.ts +++ b/libs/ui/src/lib/logo/logo.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -9,7 +8,6 @@ import { @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-logo', styleUrls: ['./logo.component.scss'], diff --git a/libs/ui/src/lib/membership-card/membership-card.component.html b/libs/ui/src/lib/membership-card/membership-card.component.html index 9faac0d3d..ab014901b 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.html +++ b/libs/ui/src/lib/membership-card/membership-card.component.html @@ -1,5 +1,5 @@
    -
    + diff --git a/libs/ui/src/lib/membership-card/membership-card.component.ts b/libs/ui/src/lib/membership-card/membership-card.component.ts index be223758d..96260ae6f 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.ts +++ b/libs/ui/src/lib/membership-card/membership-card.component.ts @@ -1,6 +1,5 @@ import { publicRoutes } from '@ghostfolio/common/routes/routes'; -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -19,13 +18,7 @@ import { GfLogoComponent } from '../logo'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - CommonModule, - GfLogoComponent, - IonIcon, - MatButtonModule, - RouterModule - ], + imports: [GfLogoComponent, IonIcon, MatButtonModule, RouterModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-membership-card', styleUrls: ['./membership-card.component.scss'], diff --git a/libs/ui/src/lib/page-tabs/index.ts b/libs/ui/src/lib/page-tabs/index.ts new file mode 100644 index 000000000..a7b3468fd --- /dev/null +++ b/libs/ui/src/lib/page-tabs/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces/interfaces'; +export * from './page-tabs.component'; diff --git a/libs/common/src/lib/interfaces/tab-configuration.interface.ts b/libs/ui/src/lib/page-tabs/interfaces/interfaces.ts similarity index 100% rename from libs/common/src/lib/interfaces/tab-configuration.interface.ts rename to libs/ui/src/lib/page-tabs/interfaces/interfaces.ts diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.html b/libs/ui/src/lib/page-tabs/page-tabs.component.html new file mode 100644 index 000000000..fa9af9b11 --- /dev/null +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.html @@ -0,0 +1,30 @@ + + + + + diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.scss b/libs/ui/src/lib/page-tabs/page-tabs.component.scss new file mode 100644 index 000000000..920b00ae9 --- /dev/null +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.scss @@ -0,0 +1,74 @@ +@use '@angular/material' as mat; + +:host { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + + @include mat.tabs-overrides( + ( + active-indicator-height: 0, + divider-height: 0, + label-text-tracking: normal + ) + ); + + ::ng-deep { + .fab-container { + @media (max-width: 575.98px) { + bottom: 5rem; + } + } + + .mat-mdc-tab-nav-panel { + padding: 2rem 0; + + @media (max-width: 575.98px) { + padding: 1rem 0; + } + } + } + + @media (max-width: 575.98px) { + @include mat.tabs-overrides( + ( + container-height: 3rem + ) + ); + } + + @media (min-width: 576px) { + flex-direction: row-reverse; + + @include mat.tabs-overrides( + ( + container-height: 2rem + ) + ); + + ::ng-deep { + .mat-mdc-tab-header { + background-color: rgba(var(--palette-foreground-base), 0.02); + padding: 2rem 0; + width: 14rem; + + .mat-mdc-tab-links { + flex-direction: column; + + .mat-mdc-tab-link { + justify-content: flex-start; + } + } + } + } + } +} + +:host-context(.theme-dark) { + @media (min-width: 576px) { + .mat-mdc-tab-header { + background-color: rgba(var(--palette-foreground-base-dark), 0.02); + } + } +} diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.ts b/libs/ui/src/lib/page-tabs/page-tabs.component.ts new file mode 100644 index 000000000..61c2caf05 --- /dev/null +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.ts @@ -0,0 +1,30 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input +} from '@angular/core'; +import { MatTabsModule } from '@angular/material/tabs'; +import { RouterModule } from '@angular/router'; +import { IonIcon } from '@ionic/angular/standalone'; +import { DeviceDetectorService } from 'ngx-device-detector'; + +import { TabConfiguration } from './interfaces/interfaces'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [IonIcon, MatTabsModule, RouterModule], + selector: 'gf-page-tabs', + styleUrls: ['./page-tabs.component.scss'], + templateUrl: './page-tabs.component.html' +}) +export class GfPageTabsComponent { + public deviceType: string; + public readonly tabs = input.required(); + + private readonly deviceService = inject(DeviceDetectorService); + + public constructor() { + this.deviceType = this.deviceService.getDeviceInfo().deviceType; + } +} diff --git a/libs/ui/src/lib/toggle/toggle.component.html b/libs/ui/src/lib/toggle/toggle.component.html index d6271ef58..3fe7d00ba 100644 --- a/libs/ui/src/lib/toggle/toggle.component.html +++ b/libs/ui/src/lib/toggle/toggle.component.html @@ -6,12 +6,12 @@ @for (option of options(); track option) { {{ option.label }} diff --git a/libs/ui/src/lib/toggle/toggle.component.ts b/libs/ui/src/lib/toggle/toggle.component.ts index db7c45487..9324b3be7 100644 --- a/libs/ui/src/lib/toggle/toggle.component.ts +++ b/libs/ui/src/lib/toggle/toggle.component.ts @@ -1,6 +1,5 @@ import { ToggleOption } from '@ghostfolio/common/interfaces'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -13,7 +12,7 @@ import { MatRadioModule } from '@angular/material/radio'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, MatRadioModule, ReactiveFormsModule], + imports: [MatRadioModule, ReactiveFormsModule], selector: 'gf-toggle', styleUrls: ['./toggle.component.scss'], templateUrl: './toggle.component.html' diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html index 06b27b97f..1f9418230 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -120,8 +120,13 @@ 0, expanded: element.expand ?? false }" @@ -150,7 +155,7 @@ *matRowDef="let row; columns: ['expandedDetail']" class="holding-detail" mat-row - [ngClass]="{ 'd-none': !row.parents?.length }" + [class.d-none]="!row.parents?.length" >
    diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 7c9ae033f..51ef8751f 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -12,7 +12,6 @@ import { transition, trigger } from '@angular/animations'; -import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, @@ -44,7 +43,6 @@ import { GfValueComponent } from '../value/value.component'; ], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, GfSymbolPipe, GfValueComponent, MatButtonModule, @@ -86,8 +84,8 @@ export class GfTopHoldingsComponent implements OnChanges { } } - public onClickHolding(assetProfileIdentifier: AssetProfileIdentifier) { - this.holdingClicked.emit(assetProfileIdentifier); + public onClickHolding({ dataSource, symbol }: AssetProfileIdentifier) { + this.holdingClicked.emit({ dataSource, symbol }); } public onShowAllHoldings() { diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts index c8951ce6b..e98b85252 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts @@ -1,3 +1,5 @@ +import { DEFAULT_COLOR_SCHEME } from '@ghostfolio/common/config'; + import { CommonModule } from '@angular/common'; import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; @@ -37,7 +39,7 @@ export const Default: Story = { args: { holdings, baseCurrency: 'USD', - colorScheme: 'LIGHT', + colorScheme: DEFAULT_COLOR_SCHEME, cursor: undefined, dateRange: 'mtd', locale: 'en-US' diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 7069cabb0..910914230 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -280,8 +280,8 @@ export class GfTreemapChartComponent ); } - const name = raw._data.name; - const symbol = raw._data.symbol; + const name = raw._data.assetProfile.name; + const symbol = raw._data.assetProfile.symbol; return [ isUUID(symbol) ? (name ?? symbol) : symbol, @@ -322,8 +322,10 @@ export class GfTreemapChartComponent ['desc'] ) as PortfolioPosition[]; - const dataSource: DataSource = dataset[dataIndex].dataSource; - const symbol: string = dataset[dataIndex].symbol; + const dataSource: DataSource = + dataset[dataIndex].assetProfile.dataSource; + + const symbol: string = dataset[dataIndex].assetProfile.symbol; this.treemapChartClicked.emit({ dataSource, symbol }); } catch {} @@ -357,10 +359,12 @@ export class GfTreemapChartComponent callbacks: { label: ({ raw }: GfTreemapTooltipItem) => { const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; - const name = raw._data.name; + const name = raw._data.assetProfile.name; + const sign = raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; - const symbol = raw._data.symbol; + + const symbol = raw._data.assetProfile.symbol; const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; diff --git a/libs/ui/src/lib/trend-indicator/trend-indicator.component.html b/libs/ui/src/lib/trend-indicator/trend-indicator.component.html index b9f65a2ea..7cf6210f8 100644 --- a/libs/ui/src/lib/trend-indicator/trend-indicator.component.html +++ b/libs/ui/src/lib/trend-indicator/trend-indicator.component.html @@ -16,7 +16,7 @@ } @else if (value > -0.0005 && value < 0.0005) { @@ -29,7 +29,7 @@ } diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index 48e9c02e7..d5476f42d 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -27,7 +27,7 @@ @if (value || value === 0 || value === null) {
    @if (isNumber || value === null) { @if (colorizeSign && !useAbsoluteValue) { @@ -40,10 +40,9 @@ }
    @if (value === null) { ***** @@ -60,10 +59,9 @@ @if (isString) {
    {{ formattedValue }}
    diff --git a/package-lock.json b/package-lock.json index f507d8eb8..02044ceb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.2.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.2.0", + "version": "3.5.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -21,14 +21,14 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "6.20.3", - "@bull-board/express": "6.20.3", - "@bull-board/nestjs": "6.20.3", + "@bull-board/api": "7.1.5", + "@bull-board/express": "7.1.5", + "@bull-board/nestjs": "7.1.5", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", - "@internationalized/number": "3.6.5", - "@ionic/angular": "8.8.1", - "@keyv/redis": "4.4.0", + "@internationalized/number": "3.6.6", + "@ionic/angular": "8.8.5", + "@keyv/redis": "5.1.6", "@nestjs/bull": "11.0.4", "@nestjs/cache-manager": "3.1.0", "@nestjs/common": "11.1.19", @@ -40,12 +40,12 @@ "@nestjs/platform-express": "11.1.19", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", - "@openrouter/ai-sdk-provider": "0.7.2", - "@prisma/adapter-pg": "7.7.0", - "@prisma/client": "7.7.0", + "@openrouter/ai-sdk-provider": "2.9.0", + "@prisma/adapter-pg": "7.8.0", + "@prisma/client": "7.8.0", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "ai": "4.3.16", + "ai": "6.0.174", "alphavantage": "2.2.0", "big.js": "7.0.1", "bootstrap": "4.6.2", @@ -60,7 +60,7 @@ "class-validator": "0.15.1", "color": "5.0.3", "cookie-parser": "1.4.7", - "countries-and-timezones": "3.8.0", + "countries-and-timezones": "3.9.0", "countries-list": "3.3.0", "countup.js": "2.10.0", "date-fns": "4.1.0", @@ -68,7 +68,7 @@ "dotenv-expand": "12.0.3", "envalid": "8.1.1", "fast-redact": "3.5.0", - "fuse.js": "7.1.0", + "fuse.js": "7.3.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", "http-status-codes": "2.3.0", @@ -82,7 +82,7 @@ "ngx-markdown": "21.2.0", "ngx-skeleton-loader": "12.0.0", "open-color": "1.9.1", - "papaparse": "5.3.1", + "papaparse": "5.5.3", "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-headerapikey": "1.2.2", @@ -113,16 +113,16 @@ "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", "@nestjs/testing": "11.1.19", - "@nx/angular": "22.6.5", - "@nx/eslint-plugin": "22.6.5", - "@nx/jest": "22.6.5", - "@nx/js": "22.6.5", - "@nx/module-federation": "22.6.5", - "@nx/nest": "22.6.5", - "@nx/node": "22.6.5", - "@nx/storybook": "22.6.5", - "@nx/web": "22.6.5", - "@nx/workspace": "22.6.5", + "@nx/angular": "22.7.2", + "@nx/eslint-plugin": "22.7.2", + "@nx/jest": "22.7.2", + "@nx/js": "22.7.2", + "@nx/module-federation": "22.7.2", + "@nx/nest": "22.7.2", + "@nx/node": "22.7.2", + "@nx/storybook": "22.7.2", + "@nx/web": "22.7.2", + "@nx/workspace": "22.7.2", "@schematics/angular": "21.2.6", "@storybook/addon-docs": "10.1.10", "@storybook/addon-themes": "10.1.10", @@ -136,7 +136,7 @@ "@types/jsonpath": "0.2.4", "@types/lodash": "4.17.24", "@types/node": "22.15.17", - "@types/papaparse": "5.3.7", + "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", "@typescript-eslint/eslint-plugin": "8.43.0", @@ -149,10 +149,10 @@ "jest": "30.2.0", "jest-environment-jsdom": "30.2.0", "jest-preset-angular": "16.0.0", - "nx": "22.6.5", + "nx": "22.7.2", "prettier": "3.8.3", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.7.0", + "prisma": "7.8.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "8.4.0", @@ -161,8 +161,7 @@ "ts-jest": "29.4.0", "ts-node": "10.9.2", "tslib": "2.8.1", - "typescript": "5.9.2", - "webpack-bundle-analyzer": "4.10.2" + "typescript": "5.9.2" }, "engines": { "node": ">=22.18.0" @@ -175,74 +174,50 @@ "dev": true, "license": "MIT" }, - "node_modules/@ai-sdk/provider": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", - "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "node_modules/@ai-sdk/gateway": { + "version": "3.0.109", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.109.tgz", + "integrity": "sha512-r6dOqThjODp1vOhGRJg2OCmyB/ZOQtGx1esZ2SDvwDX5XoX8dBqYaYjLg8MPXTzMGJSgOkJyCxWgUcZtAl16pw==", "license": "Apache-2.0", "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/provider-utils": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", - "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "1.1.3", - "nanoid": "^3.3.8", - "secure-json-parse": "^2.7.0" + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.26", + "@vercel/oidc": "3.2.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "zod": "^3.23.8" + "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/react": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz", - "integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==", + "node_modules/@ai-sdk/provider": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", + "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "2.2.8", - "@ai-sdk/ui-utils": "1.2.11", - "swr": "^2.2.5", - "throttleit": "2.1.0" + "json-schema": "^0.4.0" }, "engines": { "node": ">=18" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } } }, - "node_modules/@ai-sdk/ui-utils": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", - "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.26.tgz", + "integrity": "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.8", - "zod-to-json-schema": "^3.24.1" + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" }, "engines": { "node": ">=18" }, "peerDependencies": { - "zod": "^3.23.8" + "zod": "^3.25.76 || ^4.1.8" } }, "node_modules/@algolia/abtesting": { @@ -3547,36 +3522,48 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bull-board/api": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.20.3.tgz", - "integrity": "sha512-cDrsJJsmF4DbbY8/5oHxO4qFtyFjxexsWQKHowsud/8H4mtZN7MZg4fCmNzfaxc9Ov7V6r9Y9F5G2Mq6t7ZEJg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", + "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "6.20.3" + "@bull-board/ui": "7.1.5" } }, "node_modules/@bull-board/express": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.20.3.tgz", - "integrity": "sha512-S6BGeSf/PLwjx5W1IrKxoV8G6iiMmLqT/pldZ6BiC1IDldedisTtAdL1z117swXPv1H7/3hy0vr03dUr8bUCPg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.1.5.tgz", + "integrity": "sha512-kp4SzhVjZlykryiQwcOhJjDhiLbBnZoAMoSgEstzqQ0raLw+jERRC6ryJ0MIQO+SO+Jv9EjjxrXCR8O2YSP/eg==", "license": "MIT", "dependencies": { - "@bull-board/api": "6.20.3", - "@bull-board/ui": "6.20.3", - "ejs": "^3.1.10", + "@bull-board/api": "7.1.5", + "@bull-board/ui": "7.1.5", + "ejs": "^5.0.2", "express": "^5.2.1" } }, + "node_modules/@bull-board/express/node_modules/ejs": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", + "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, "node_modules/@bull-board/nestjs": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-6.20.3.tgz", - "integrity": "sha512-VFi96Z2M8k3G26H1ivzQnpjKszxh90vrUm78VtMZH/sh8wjm88mJFDXcOgFutOaddx7cc9VNXlKsTTcu6okPFQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.1.5.tgz", + "integrity": "sha512-1y+HkjnDaZoSCXJRsiYfBNBVx+PX3I8x3Uv+SSJuSpt2vHifMRwFbChO3XDxeWXetT1eR+yqPVq6ub5eJwNOYQ==", "license": "MIT", "peerDependencies": { - "@bull-board/api": "^6.20.3", + "@bull-board/api": "^7.1.5", "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", @@ -3585,12 +3572,12 @@ } }, "node_modules/@bull-board/ui": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.20.3.tgz", - "integrity": "sha512-oANyYoW0X+xd0j/09DRyh3u7Q3wqBtXiLEWyZUJIi/Bjp/hINwiw20RwWuRcaFkqkFylEJL9l+pjmeSA9X5L2A==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", + "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", "license": "MIT", "dependencies": { - "@bull-board/api": "6.20.3" + "@bull-board/api": "7.1.5" } }, "node_modules/@cacheable/utils": { @@ -3661,9 +3648,9 @@ } }, "node_modules/@colordx/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.2.0.tgz", - "integrity": "sha512-wifnqsGCXRh+lJdX4975nKEPJaSk7k8rMiA/VeGrS4tOTn06WZrow6cUA7wFJKPXfcqj0rXeH4BMgGoKZvBf7g==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.4.3.tgz", + "integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==", "dev": true, "license": "MIT" }, @@ -4996,21 +4983,21 @@ } }, "node_modules/@internationalized/number": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", - "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz", + "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@ionic/angular": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.8.1.tgz", - "integrity": "sha512-Jp7LbouSHAnR00Dsa8qE1CSOZNqAfBCO0XKXScJNz8NKVoZe5fPGy/CboehGtAQ1xgzh2eDa15zMmyetXjAkYA==", + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.8.5.tgz", + "integrity": "sha512-wUYKPhLzyrlRIoqM5lk0wCx2CCebxHdQBMXZdBQBvE69XlqEmkoxbbPW0YpYJvY5wkhZaDa2lzLIHo/lfijqqQ==", "license": "MIT", "dependencies": { - "@ionic/core": "8.8.1", + "@ionic/core": "8.8.5", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -5024,9 +5011,9 @@ } }, "node_modules/@ionic/core": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.1.tgz", - "integrity": "sha512-ksOUHyOEqoyUIVWcwCNSFZVGwNfP1DKrUVeN/Cdk/Xl9Rdd/5MLHGsrOQpWQfoCf3Csdnw+KHHPrXz/2fzMkMA==", + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.5.tgz", + "integrity": "sha512-gVaEeLVNn6QGTmxmiC5J82k0WYEqDSuP6Hxmf4zHsBQptlIDmVOmbqzz8BIevQgb8q/s7ykwE0NGyqRu4BVsqg==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -7157,19 +7144,20 @@ } }, "node_modules/@keyv/redis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-4.4.0.tgz", - "integrity": "sha512-n/KEj3S7crVkoykggqsMUtcjNGvjagGPlJYgO/r6m9hhGZfhp1txJElHxcdJ1ANi/LJoBuOSILj15g6HD2ucqQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-5.1.6.tgz", + "integrity": "sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==", "license": "MIT", "dependencies": { - "@redis/client": "^1.6.0", - "cluster-key-slot": "^1.1.2" + "@redis/client": "^5.10.0", + "cluster-key-slot": "^1.1.2", + "hookified": "^1.13.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "keyv": "^5.3.3" + "keyv": "^5.6.0" } }, "node_modules/@keyv/serialize": { @@ -7393,13 +7381,13 @@ } }, "node_modules/@module-federation/bridge-react-webpack-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.3.3.tgz", - "integrity": "sha512-W2jQ3Wuqree9Dq3UAx8jGbYtvHuuYgzrd2j9FP8Bt6NaynaNU1yYG86MBnAhZJPTltex0CguudR1dgFpYdvLUg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-yxDv/FJoLiKo2eqIcEWvSnSpJgyYkCzJvNaFsQ2QE3rNv68IeAarlSzCo+d0QyQoPJnTETyHsOh1SSBazIzecw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "2.3.3", + "@module-federation/sdk": "2.4.0", "@types/semver": "7.5.8", "semver": "7.6.3" } @@ -7418,14 +7406,14 @@ } }, "node_modules/@module-federation/cli": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.3.3.tgz", - "integrity": "sha512-g3f3aEruv07zK4VcUlAllswrp2ncA/jF0P0yoEWNRa9K7N+xNCfqcdzw2aVWOJ30qNMurhLWuyzYqfDIx0LpfQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.4.0.tgz", + "integrity": "sha512-c46g9srroc2hDfrlHyd4Y404SLnw3v9t7Kqij+yK01Hx8C2FyZpyanTGUHVyrmzqp/0y3lPrWURUHkHfk/cJQA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/dts-plugin": "2.4.0", + "@module-federation/sdk": "2.4.0", "commander": "11.1.0", "jiti": "2.4.2" }, @@ -7436,40 +7424,17 @@ "node": ">=16.0.0" } }, - "node_modules/@module-federation/data-prefetch": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-2.3.3.tgz", - "integrity": "sha512-ZM1QtyjbWYnhUizHFhwYjHGXlkZek3vzTpL35d5FkAhVrOU0u0Qv6zpZjdcCm0FJznqVsUQx1w0vagUyGWQf0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/@module-federation/dts-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.3.3.tgz", - "integrity": "sha512-VNtURt+hvieNKCBleAqHKffLAU4clKmuuqLQIbvDkFbGe4bo7hUaq5DruTnJBWWDOizZx0OQrdQYPijCnBK6UQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.4.0.tgz", + "integrity": "sha512-sa6v5ByyqMRHzpwDu0zc7s5mZ39EFIkG0jkRfZU09pzkrJEIy4uZ1Kt9SLysFB8RBMIAvAakAfqDlVWvf1lndg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/sdk": "2.3.3", - "@module-federation/third-party-dts-extractor": "2.3.3", + "@module-federation/error-codes": "2.4.0", + "@module-federation/managers": "2.4.0", + "@module-federation/sdk": "2.4.0", + "@module-federation/third-party-dts-extractor": "2.4.0", "adm-zip": "0.5.10", "ansi-colors": "4.1.3", "isomorphic-ws": "5.0.0", @@ -7498,24 +7463,23 @@ } }, "node_modules/@module-federation/enhanced": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.3.3.tgz", - "integrity": "sha512-BJSs56lqO9NI9aC+hVhg2CU/UwG1TphVl1b7WBx6Jv6DYUyVQbgXeQpgqYVsxYVRKYOl7eDZmjXl2eA/n1IP/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "2.3.3", - "@module-federation/cli": "2.3.3", - "@module-federation/data-prefetch": "2.3.3", - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/error-codes": "2.3.3", - "@module-federation/inject-external-runtime-core-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/manifest": "2.3.3", - "@module-federation/rspack": "2.3.3", - "@module-federation/runtime-tools": "2.3.3", - "@module-federation/sdk": "2.3.3", - "@module-federation/webpack-bundler-runtime": "2.3.3", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.4.0.tgz", + "integrity": "sha512-NiccK03x7V6bK2LvJNuW520kT+Onx+LJe8lyPsENjXctECCIFJdJOmYr8ABif/kLayWKrrYCzCGVNNiQXANEGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "2.4.0", + "@module-federation/cli": "2.4.0", + "@module-federation/dts-plugin": "2.4.0", + "@module-federation/error-codes": "2.4.0", + "@module-federation/inject-external-runtime-core-plugin": "2.4.0", + "@module-federation/managers": "2.4.0", + "@module-federation/manifest": "2.4.0", + "@module-federation/rspack": "2.4.0", + "@module-federation/runtime-tools": "2.4.0", + "@module-federation/sdk": "2.4.0", + "@module-federation/webpack-bundler-runtime": "2.4.0", "schema-utils": "4.3.0", "tapable": "2.3.0", "upath": "2.0.1" @@ -7579,56 +7543,56 @@ } }, "node_modules/@module-federation/error-codes": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.3.3.tgz", - "integrity": "sha512-UVtKBoKnRDcHgByIDvPRZSxQqjqbNH7NvJm1KHLoce33+EDiIdZYs0HvvUQv43RgESpB9s7HjrqFlq3bEcAgfQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.4.0.tgz", + "integrity": "sha512-ktCZtwOoiKR1URJyBt223OsOFAUvc13rICYif55mt7+DomtELlh5FicnEz6mPLBUwmNM9vyBMvkxOdp+fQ5oUg==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/inject-external-runtime-core-plugin": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.3.3.tgz", - "integrity": "sha512-ImSft6hOkMdnpZX8O+RydwkYENxhAwT92n1OAT3Xf01DXMrEpSO0PqBlPGgontxuiaV9dM2/xWSLGuIWaOtupA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.4.0.tgz", + "integrity": "sha512-GucUMQmQXcnJC/OnJGvMz3Qy7ap8nAffhQPwDpOSi0Qwm+Iq/ppzG8N3tlLBDmv/O8hiF8HHlg789XK2kcCQtg==", "dev": true, "license": "MIT", "peerDependencies": { - "@module-federation/runtime-tools": "2.3.3" + "@module-federation/runtime-tools": "2.4.0" } }, "node_modules/@module-federation/managers": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.3.3.tgz", - "integrity": "sha512-sYL0t2guakJ+nDSQANH54uz5q1YxaNCn5C3lr+7BoRD49dX7Z6k7094yqOPEy8trzqdIoQVFpgewVA6IC/FeyQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.4.0.tgz", + "integrity": "sha512-Z8j6aog44G1gt4yIAaeDowwZ7xg0aAxTA1Hq69euJK9cR9MDEaLbLUk57jDoiRj6xLwlCiw7ozY+U15BQATk6Q==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "2.3.3", + "@module-federation/sdk": "2.4.0", "find-pkg": "2.0.0" } }, "node_modules/@module-federation/manifest": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.3.3.tgz", - "integrity": "sha512-mAEXuo5sGt8FUDzftU8f0ci0PbsZIDcLRYX9AkXwbXg0JRyVEvWyiBrEKF+zZuy7YM7eRdyp6JjLJPDzufhj5w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.4.0.tgz", + "integrity": "sha512-ZL+W5rbtgRf9TWRP7Dupt/Svia4bJEOS6gWSj9jzemiLPRPkMO5hjWZKVHIc8oG+Vb25yzozFMmQ+luGi695wg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/dts-plugin": "2.4.0", + "@module-federation/managers": "2.4.0", + "@module-federation/sdk": "2.4.0", "find-pkg": "2.0.0" } }, "node_modules/@module-federation/node": { - "version": "2.7.41", - "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.41.tgz", - "integrity": "sha512-ZaNrfX+7ua8UvnRa6qgrDtViU9Oz3oBGpFprldU8ek0NB2QWNACu9RMU7fHdTD/FKlzGVLi/LgdKnNKkmJD2TA==", + "version": "2.7.42", + "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.42.tgz", + "integrity": "sha512-aX/T4L9bPbOgNLIW+30k/dA2Iohoy9/jf4yG1ka6Hkuo5h7iEBeZiQkwIqC06cnCbtKL1HnAiYlXHmrDPW5xvg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/enhanced": "2.3.3", - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3", + "@module-federation/enhanced": "2.4.0", + "@module-federation/runtime": "2.4.0", + "@module-federation/sdk": "2.4.0", "encoding": "0.1.13", "node-fetch": "2.7.0", "tapable": "2.3.0" @@ -7642,66 +7606,20 @@ } } }, - "node_modules/@module-federation/node/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@module-federation/node/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@module-federation/node/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/@module-federation/node/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/@module-federation/rspack": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.3.3.tgz", - "integrity": "sha512-4s3G+wXZ6J3rKe0EeZnGLQUM7y+qpiI5NM3U6ylZuxD8q7mAwQVHThbH6ruDYUNDVEOc6N0j/+/LdfGRw+e5xw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.4.0.tgz", + "integrity": "sha512-NWH5Vaj/fA9R7PfbwTuE1Ty/pfiAt12On0E3FzoeVPCyb5MxO1i0z+xxRHbPhF4ZOrAPGEMaMQ8Z9vH94EiElw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "2.3.3", - "@module-federation/dts-plugin": "2.3.3", - "@module-federation/inject-external-runtime-core-plugin": "2.3.3", - "@module-federation/managers": "2.3.3", - "@module-federation/manifest": "2.3.3", - "@module-federation/runtime-tools": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/bridge-react-webpack-plugin": "2.4.0", + "@module-federation/dts-plugin": "2.4.0", + "@module-federation/inject-external-runtime-core-plugin": "2.4.0", + "@module-federation/managers": "2.4.0", + "@module-federation/manifest": "2.4.0", + "@module-federation/runtime-tools": "2.4.0", + "@module-federation/sdk": "2.4.0" }, "peerDependencies": { "@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0", @@ -7718,47 +7636,47 @@ } }, "node_modules/@module-federation/runtime": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.3.3.tgz", - "integrity": "sha512-JYJ3qv9V85DtBtT/ppDuJNwBTUrYqqZDYcyiTzwY5+44dC5QPvgJ//F+BOhAhZ02WkZV0b4jsKTyLOC3vXKGqQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.4.0.tgz", + "integrity": "sha512-IrLAMwUuteRgFlEkg9jrn4bk8uC897FnXvfNmkKD8/qIoNtSd+32e5ouQn+PEYbX/RjRUB1TYveY6rYHpTPkyg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/runtime-core": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.4.0", + "@module-federation/runtime-core": "2.4.0", + "@module-federation/sdk": "2.4.0" } }, "node_modules/@module-federation/runtime-core": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.3.3.tgz", - "integrity": "sha512-B07LDH9KxhBO3GbULGW64mQFVQBtrEd3PoaCBm7XR1IbU8rMQUJQjDNVZgXYcyhRPBVP+3KWZuiaKFRiNb6PQw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.4.0.tgz", + "integrity": "sha512-0S8fDw28DXDW17lTQwq5vfJWe2lG0Lw3+w4vk3DVVImLwXXay+OGxLDxzWUfypWcMznfpnoAnFUMO3PtuXziuA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.4.0", + "@module-federation/sdk": "2.4.0" } }, "node_modules/@module-federation/runtime-tools": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.3.3.tgz", - "integrity": "sha512-XODzyLbBYcy4wnYBXKIBqaHPVfBx1HshGdjZmSctDDnx9/VYgdx9DShb6UI+WuQBKJgPzTcx4xbvbCM4SdMilQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.4.0.tgz", + "integrity": "sha512-BWQsGT4EWscV9bx3bVHEwp6lERBsiYm7rnPiDpwd2fx+hGEpz1IM9Pz35VryHNDXYxw7MzaAuwTMM+L7uN8OYQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "2.3.3", - "@module-federation/webpack-bundler-runtime": "2.3.3" + "@module-federation/runtime": "2.4.0", + "@module-federation/webpack-bundler-runtime": "2.4.0" } }, "node_modules/@module-federation/sdk": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.3.3.tgz", - "integrity": "sha512-mwCS+LQdqiSc6fM5iz/S60ibaFNSH6kNqlZkCRIuS4yjdZ+jgnihz+6xp1QzppvfFgKLhEHBiXOmcYOdk3Ckew==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.4.0.tgz", + "integrity": "sha512-eZDdF5B69W9npuka0VL24FY7XDM+YAwwfkscSeWOSqv4/8Hm0xmcmSurlP6NIOrwbeogerRCtEcnx/TFXYjoow==", "dev": true, "license": "MIT", "peerDependencies": { - "node-fetch": "^3.3.2" + "node-fetch": "^2.7.0 || ^3.3.2" }, "peerDependenciesMeta": { "node-fetch": { @@ -7767,9 +7685,9 @@ } }, "node_modules/@module-federation/third-party-dts-extractor": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.3.3.tgz", - "integrity": "sha512-rR94TjC1QVQLQPTazI0waLc76hI8dnv6aHTl+PUEIY9s5hXp8TA85XS0QJQqIf2KTjlPgZbWAwyFjOAJluTjaw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.4.0.tgz", + "integrity": "sha512-4v24t6L3dET/6abMOM2fiM3roT0c8mi21/i+uDc6WG7U0i+Xp2SojBppTs6gnT0lkwMTe+u6xIpNQakdUftHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -7778,15 +7696,15 @@ } }, "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.3.3.tgz", - "integrity": "sha512-W+P6ZF9J3gwnQuoF07YV0OiR1D6sI/uErUu4+c3QXxka3orANUHujkddNSsDxL1obAGoJa7Da99crZKf7u2j/w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.4.0.tgz", + "integrity": "sha512-Ntx0+QsgcwtXlpGjL/Vf2PMdPjUHl07b3yM4kBc1kbRogW3Ee84QneBRi/X3w4/jlz4JKbHjD+CMXaqi2W6hgw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "2.3.3", - "@module-federation/runtime": "2.3.3", - "@module-federation/sdk": "2.3.3" + "@module-federation/error-codes": "2.4.0", + "@module-federation/runtime": "2.4.0", + "@module-federation/sdk": "2.4.0" } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { @@ -9045,21 +8963,21 @@ } }, "node_modules/@nx/angular": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-22.6.5.tgz", - "integrity": "sha512-NPkrGGatlUUK7twHKYpv3mv6jYL6dRiqdPuqAhQfmUnuz5lA4ZhpCfwEBKUFEKsGNwOft0ZCGZdSdODliaKZzA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-22.7.2.tgz", + "integrity": "sha512-+HCggLwJXp55ZdKrn0VkYfw9gGgZpiIHdlY8m3KnwJzdA+Tfl9t10JvidFXprk7gmnRaU8hHidfz6e1juG6D6g==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/eslint": "22.6.5", - "@nx/js": "22.6.5", - "@nx/module-federation": "22.6.5", - "@nx/rspack": "22.6.5", - "@nx/web": "22.6.5", - "@nx/webpack": "22.6.5", - "@nx/workspace": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/devkit": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/js": "22.7.2", + "@nx/module-federation": "22.7.2", + "@nx/rspack": "22.7.2", + "@nx/web": "22.7.2", + "@nx/webpack": "22.7.2", + "@nx/workspace": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "@typescript-eslint/type-utils": "^8.0.0", "enquirer": "~2.3.6", "magic-string": "~0.30.2", @@ -9106,17 +9024,17 @@ } }, "node_modules/@nx/cypress": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-22.6.5.tgz", - "integrity": "sha512-8pVXkVryoLRDEFjKrOIcPArY8RObf7SC1U8WMyxNS28Hs32eaENWNXtOncrXQz98jWhpi4jvmr0WPLCr9NCFsA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-22.7.2.tgz", + "integrity": "sha512-ivrwIXNTn0p9nGg2z3mjZJYPuH7X+O3eMtuqyPglkxKlAhuUQXVmKYB9nIqRMGR2nGCi9cDC7J38h+0Py+TunA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/eslint": "22.6.5", - "@nx/js": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", - "detect-port": "^1.5.1", + "@nx/devkit": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/js": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", + "detect-port": "^2.1.0", "semver": "^7.6.3", "tree-kill": "1.2.2", "tslib": "^2.3.0" @@ -9131,16 +9049,16 @@ } }, "node_modules/@nx/devkit": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.6.5.tgz", - "integrity": "sha512-9kvAI+kk2pfEXLqS8OyjI9XvWmp+Gdn7jPfxDAz8BOqxMyPy3p5hYl+jc4TIsLOWunAFl8azqrcYsHzEpaWCIA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.7.2.tgz", + "integrity": "sha512-oE2SFUxQeZm/EmFABHpWQ4Pi0fBKbJbXKGPvdFaHoMumRxhqBhuBVf/ap5kYFg8Y9bK/zHJkpsEbGyiyRrhvog==", "dev": true, "license": "MIT", "dependencies": { "@zkochan/js-yaml": "0.0.7", "ejs": "5.0.1", "enquirer": "~2.3.6", - "minimatch": "10.2.4", + "minimatch": "10.2.5", "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" @@ -9160,9 +9078,9 @@ } }, "node_modules/@nx/devkit/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -9186,13 +9104,13 @@ } }, "node_modules/@nx/devkit/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9202,55 +9120,59 @@ } }, "node_modules/@nx/docker": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/docker/-/docker-22.6.5.tgz", - "integrity": "sha512-kEZi8sC7hL5WsUCJkmj7hvrTahK8yOsRfgRljbSQnQ6vAZPHHZQnB36ybgrb0JbhFPciRV5Mf9UQm13Ev49M2Q==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/docker/-/docker-22.7.2.tgz", + "integrity": "sha512-VSORTGE28czjDePM5XvNnbwneowlT/6N0t0Jhh6cJtSGCCWwaT4WQb8uVYOgchDr77HwOGhgezI79mmoKHWnsw==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", + "@nx/devkit": "22.7.2", "enquirer": "~2.3.6", "tslib": "^2.3.0" } }, "node_modules/@nx/eslint": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-22.6.5.tgz", - "integrity": "sha512-rEV8CveVA3CCW8MHSKauUI+6XSpQ0nZ/z64fBvBulLUoUO10/mVpkbl3NpRyhCKXzOHYhW35wwuzq6YrfSi6gA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-22.7.2.tgz", + "integrity": "sha512-LDWFg6CNtORnEnwB3XSJBjm8QnheN3F9HxE/kq69Fx+4drkSYEXjRx+27M+9kSP1z2HriSQn5LjEmz1yQD8stA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", "semver": "^7.6.3", "tslib": "^2.3.0", "typescript": "~5.9.2" }, "peerDependencies": { + "@nx/jest": "22.7.2", "@zkochan/js-yaml": "0.0.7", "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { + "@nx/jest": { + "optional": true + }, "@zkochan/js-yaml": { "optional": true } } }, "node_modules/@nx/eslint-plugin": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-22.6.5.tgz", - "integrity": "sha512-G1DkvBMzBAqxxrJ7Zfky5HN4HQjrULKZQ5J5YCnTm5qZpah58U7xAP4g8D0aJzKMWMUJkgXAU1ojiLbeZUDJkw==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-22.7.2.tgz", + "integrity": "sha512-OgfyUt4dUrlTHcnygVLXcxP0KH7yOAaB9pfdWLe86QRWm2Ei4sRdACltPRXoa9tDeEa4EdBvDTccw0AZ3UnrvQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "@typescript-eslint/type-utils": "^8.0.0", "@typescript-eslint/utils": "^8.0.0", "chalk": "^4.1.0", "confusing-browser-globals": "^1.0.9", - "globals": "^15.9.0", + "globals": "^17.0.0", "jsonc-eslint-parser": "^2.1.0", "semver": "^7.6.3", "tslib": "^2.3.0" @@ -9266,9 +9188,9 @@ } }, "node_modules/@nx/eslint-plugin/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -9279,22 +9201,22 @@ } }, "node_modules/@nx/jest": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-22.6.5.tgz", - "integrity": "sha512-Px+ROXwl3s8tjS3OEVMtNY5krQ2zAWyK7s922zmtbXjNKGZ8bg6krnkq7n5XpQwyXTZNVxku8TXcN7jXZECGFQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-22.7.2.tgz", + "integrity": "sha512-t+UYRCUUT7BYoRohjf6lWVzeeITjytclxE1ENEzU0+PCAKYN8yJfAWLSsLfK4YDDBv2lTXyzHo7b5Pxpbmz+Qw==", "dev": true, "license": "MIT", "dependencies": { "@jest/reporters": "^30.0.2", "@jest/test-result": "^30.0.2", - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "identity-obj-proxy": "3.0.0", "jest-config": "^30.0.2", "jest-resolve": "^30.0.2", "jest-util": "^30.0.2", - "minimatch": "10.2.4", + "minimatch": "10.2.5", "picocolors": "^1.1.0", "resolve.exports": "2.0.3", "semver": "^7.6.3", @@ -9313,9 +9235,9 @@ } }, "node_modules/@nx/jest/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -9326,13 +9248,13 @@ } }, "node_modules/@nx/jest/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9342,9 +9264,9 @@ } }, "node_modules/@nx/js": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-22.6.5.tgz", - "integrity": "sha512-bmikz6qaBHfuAgsqPB/TfLIKfvI4g+EKIRAiU2FHnEtVWOKDAmSQXHFwE3rMS49jl2JLgxkdNjZHpg4g/OLy0g==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-22.7.2.tgz", + "integrity": "sha512-d1Hb/2n3QKE9rs8gRtfa/b1/GCGm1rnBFiqePivWbD/9iqerhgkbs6cg4MliLGRnD8gZgXSENLm4IW8ISOi69w==", "dev": true, "license": "MIT", "dependencies": { @@ -9355,16 +9277,16 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "22.6.5", - "@nx/workspace": "22.6.5", + "@nx/devkit": "22.7.2", + "@nx/workspace": "22.7.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-typescript-metadata": "^0.3.1", "chalk": "^4.1.0", "columnify": "^1.6.0", - "detect-port": "^1.5.1", - "ignore": "^5.0.4", + "detect-port": "^2.1.0", + "ignore": "^7.0.5", "js-tokens": "^4.0.0", "jsonc-parser": "3.2.0", "npm-run-path": "^4.0.1", @@ -9384,6 +9306,16 @@ } } }, + "node_modules/@nx/js/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@nx/js/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -9413,18 +9345,18 @@ } }, "node_modules/@nx/module-federation": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-22.6.5.tgz", - "integrity": "sha512-nQS3qFGs8lQ87ZQ8hab+oL+BfjCYjNPkGrpH4fXovnFgwaRNudnQnh2vTud1+JcUl0e+sJi/wIwZH4AB75jzSA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-22.7.2.tgz", + "integrity": "sha512-8KblqEdVw0b6uzhVSxz+RbjodN1BnHtWk1J4ndxG5XxiDLvW8bVEmpQAfn6DebsSRTIr+N/e3pah84j7xhZ9Yw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/enhanced": "^2.1.0", + "@module-federation/enhanced": "^2.3.3", "@module-federation/node": "^2.7.21", "@module-federation/sdk": "^2.1.0", - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "@nx/web": "22.6.5", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "@nx/web": "22.7.2", "@rspack/core": "1.6.8", "express": "^4.21.2", "http-proxy-middleware": "^3.0.5", @@ -9563,9 +9495,9 @@ } }, "node_modules/@nx/module-federation/node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { @@ -9577,7 +9509,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -9611,15 +9543,15 @@ } }, "node_modules/@nx/module-federation/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -9638,7 +9570,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -9766,22 +9698,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nx/module-federation/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@nx/module-federation/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -9861,41 +9777,41 @@ } }, "node_modules/@nx/nest": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-22.6.5.tgz", - "integrity": "sha512-HdQVzXEccVtoSakU+Mqib44DgbFi1JKkA0BVnUp6Fmp0/Bt0xo5l2lhP2x4qS8oKQCWHtoxNznWKLkmqGLRjig==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-22.7.2.tgz", + "integrity": "sha512-Xpja5pry0RWJ8K5t25Eh0HCe5Z6kg81oZh3Qr17iodh4jxAKCMbSpx+1j8PNmC6PFfzbhJyXY6gWOtnaCgbh3g==", "dev": true, "license": "MIT", "dependencies": { "@nestjs/schematics": "^11.0.0", - "@nx/devkit": "22.6.5", - "@nx/eslint": "22.6.5", - "@nx/js": "22.6.5", - "@nx/node": "22.6.5", + "@nx/devkit": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/js": "22.7.2", + "@nx/node": "22.7.2", "tslib": "^2.3.0" } }, "node_modules/@nx/node": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/node/-/node-22.6.5.tgz", - "integrity": "sha512-ahZRKpd6pqflRqBX4/oVSk5bAHtS2CM4I3T3c6iuTNenG8ehVxGmuJj8KpiC0tlLe3kJoR1oWQU1pDakXrVWow==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/node/-/node-22.7.2.tgz", + "integrity": "sha512-6nj6siMZy45r4hITYfHcqrOFpadYEkbfqwQP3xgTXZvTt6foX0HHoeOcv1rvaEvgdG6/DuZWQj4z8ursCnMWPw==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/docker": "22.6.5", - "@nx/eslint": "22.6.5", - "@nx/jest": "22.6.5", - "@nx/js": "22.6.5", + "@nx/devkit": "22.7.2", + "@nx/docker": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/jest": "22.7.2", + "@nx/js": "22.7.2", "kill-port": "^1.6.1", "tcp-port-used": "^1.0.2", "tslib": "^2.3.0" } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.6.5.tgz", - "integrity": "sha512-qT77Omkg5xQuL2+pDbneX2tI+XW5ZeayMylu7UUgK8OhTrAkJLKjpuYRH4xT5XBipxbDtlxmO3aLS3Ib1pKzJQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.2.tgz", + "integrity": "sha512-hu+x/IOzx+18imkFwSdtXnvB6d21qcXvc4bCqcbA9BQcUnvTnw0/11SLoasvDqy/9KLKHDWJAIPttcBkbArWVA==", "cpu": [ "arm64" ], @@ -9907,9 +9823,9 @@ ] }, "node_modules/@nx/nx-darwin-x64": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.6.5.tgz", - "integrity": "sha512-9jICxb7vfJ56y/7Yuh3b/n1QJqWxO9xnXKYEs6SO8xPoW/KomVckILGc1C6RQSs6/3ixVJC7k1Dh1wm5tKPFrg==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.2.tgz", + "integrity": "sha512-M4QPs4rjzZN51V7qiKUjJU7hLYtv/h0I/aGUedCQQZibbbDTl45sQlgBQlV/viw2dOw3K5+RxDxtMNFxAbhxQA==", "cpu": [ "x64" ], @@ -9921,9 +9837,9 @@ ] }, "node_modules/@nx/nx-freebsd-x64": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.6.5.tgz", - "integrity": "sha512-6B1wEKpqz5dI3AGMqttAVnA6M3DB/besAtuGyQiymK9ROlta1iuWgCcIYwcCQyhLn2Rx7vqj447KKcgCa8HlVw==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.2.tgz", + "integrity": "sha512-tdC2mBQ/ON9qvTs72aL3XVN7B5wd7UsiRJ/qwC2bk/PIpD0vo5c3EwxFyYXfTD60jnlV+CTFxhSVmu8S1pVsfw==", "cpu": [ "x64" ], @@ -9935,9 +9851,9 @@ ] }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.6.5.tgz", - "integrity": "sha512-xV50B8mnDPboct7JkAHftajI02s+8FszA8WTzhore+YGR+lEKHTLpucwGEaQuMlSdLplH7pQix4B4uK5pcMhZw==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.2.tgz", + "integrity": "sha512-bBHIC9xZ8L12BWkwMKbRi7+oV4UH1v1Yy8PsIvRfjS7GzYNlOAUMkJxywjF2msnkp8M8Rn29MEvzllZjdyaR7Q==", "cpu": [ "arm" ], @@ -9949,9 +9865,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.6.5.tgz", - "integrity": "sha512-2JkWuMGj+HpW6oPAvU5VdAx1afTnEbiM10Y3YOrl3fipWV4BiP5VDx762QTrfCraP4hl6yqTgvTe7F9xaby+jQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.2.tgz", + "integrity": "sha512-MBYG58VUTmLW4S2RlYmXJiV6P0P1lkiZXtiaulZOXmP5uCSXiqMgK47k56hq9GTbtW1SpyGgh02lkNdCYTbmLw==", "cpu": [ "arm64" ], @@ -9963,9 +9879,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.6.5.tgz", - "integrity": "sha512-Z/zMqFClnEyqDXouJKEPoWVhMQIif5F0YuECWBYjd3ZLwQsXGTItoh+6Wm3XF/nGMA2uLOHyTq/X7iFXQY3RzA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.2.tgz", + "integrity": "sha512-Wf4VBSJt5gEGdzX6uzZoITEYB/Y3TxjvPNT11NKfRU/m63b8/D8jCeRmr7cBTaMUlNmdH3Lf3G1PuPNGoEZ0Mg==", "cpu": [ "arm64" ], @@ -9977,9 +9893,9 @@ ] }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.6.5.tgz", - "integrity": "sha512-FlotSyqNnaXSn0K+yWw+hRdYBwusABrPgKLyixfJIYRzsy+xPKN6pON6vZfqGwzuWF/9mEGReRz+iM8PiW0XSg==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.2.tgz", + "integrity": "sha512-v3AQyfCkv9k+AWT2hy8hAGaCmFYf+G/bt4KAqnWhmXPWNhxrv9FhvTUcjpY+MY+6v7sKdhJv/3eDvtlLd9FOLg==", "cpu": [ "x64" ], @@ -9991,9 +9907,9 @@ ] }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.6.5.tgz", - "integrity": "sha512-RVOe2qcwhoIx6mxQURPjUfAW5SEOmT2gdhewvdcvX9ICq1hj5B2VarmkhTg0qroO7xiyqOqwq26mCzoV2I3NgQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.2.tgz", + "integrity": "sha512-3SMfMB7ynr8wGGTZP+/ZV7FqkCsOg1Raoka+4EtIPX66bEcBycg8FVg81DbyV+IzuKk3N+8Hl2IeY1W2btPypw==", "cpu": [ "x64" ], @@ -10005,9 +9921,9 @@ ] }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.6.5.tgz", - "integrity": "sha512-ZqurqI8VuYnsr2Kn4K4t+Gx6j/BZdf6qz/6Tv4A7XQQ6oNYVQgTqoNEFj+CCkVaIe6aIdCWpousFLqs+ZgBqYQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.2.tgz", + "integrity": "sha512-eTFTTF1JUKXu+PNOGd7KAdqyWyfvFKO/wpqHoq9fjnbjXgCdCg1PaRxHIxA1WT5HFj1iHS6Or+GC1zA1KNt0Sw==", "cpu": [ "arm64" ], @@ -10019,9 +9935,9 @@ ] }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.6.5.tgz", - "integrity": "sha512-i2QFBJIuaYg9BHxrrnBV4O7W9rVL2k0pSIdk/rRp3EYJEU93iUng+qbZiY9wh1xvmXuUCE2G7TRd+8/SG/RFKg==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.2.tgz", + "integrity": "sha512-fbVAiJ7RKSanUXrL67Z6as7BY1akznRqo71ACmrxLvLicG3UsmATbHKGp0zULoe3jBm+rNrIrLk+quZn5q0wUg==", "cpu": [ "x64" ], @@ -10033,17 +9949,17 @@ ] }, "node_modules/@nx/rspack": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-22.6.5.tgz", - "integrity": "sha512-ugjdD7OY4Cy7AcSlEJcfcfDWxev5PnVagb4FEEEutneITLz8hrBmQ+uY5cJg07Vsx6eauskpirkwRtXAQeHgFQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-22.7.2.tgz", + "integrity": "sha512-hlBIqhL9otJEQ9x8pf6CYqz0DdTKRW1w3jQ7c1hSafEHrC+OHs3UGReYeDO9Avua5eNA4/FVMLUVFUOPNzk3Wg==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "@nx/module-federation": "22.6.5", - "@nx/web": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "@nx/module-federation": "22.7.2", + "@nx/web": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "@rspack/core": "1.6.8", "@rspack/dev-server": "^1.1.4", "@rspack/plugin-react-refresh": "^1.0.0", @@ -10072,7 +9988,7 @@ "webpack-node-externals": "^3.0.0" }, "peerDependencies": { - "@module-federation/enhanced": "^2.1.0", + "@module-federation/enhanced": "^2.3.3", "@module-federation/node": "^2.7.21" } }, @@ -10202,9 +10118,9 @@ } }, "node_modules/@nx/rspack/node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { @@ -10216,7 +10132,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -10286,15 +10202,15 @@ } }, "node_modules/@nx/rspack/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -10313,7 +10229,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -10463,22 +10379,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nx/rspack/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@nx/rspack/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -10558,50 +10458,84 @@ } }, "node_modules/@nx/storybook": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-22.6.5.tgz", - "integrity": "sha512-h0O8t7ir5hkqJhvT/g0E9KiILj3y9thZ7pOlB/Krc1G2GzYynSZJPElv//pibPhxtZl0Yk4AldkX9LqMkWtfFA==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-22.7.2.tgz", + "integrity": "sha512-kf34LHK0nvyTsXG43FqBGBBPzq9Th90hdWUVktLo636K6/mDvVPTM033BPzQDIljEqWAD+34YPiCtKGLE3H1Ag==", "dev": true, "license": "MIT", "dependencies": { - "@nx/cypress": "22.6.5", - "@nx/devkit": "22.6.5", - "@nx/eslint": "22.6.5", - "@nx/js": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/cypress": "22.7.2", + "@nx/devkit": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/js": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "semver": "^7.6.3", "tslib": "^2.3.0" }, "peerDependencies": { + "@nx/web": "22.7.2", "storybook": ">=7.0.0 <11.0.0" + }, + "peerDependenciesMeta": { + "@nx/web": { + "optional": true + } } }, "node_modules/@nx/web": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/web/-/web-22.6.5.tgz", - "integrity": "sha512-LjKPLWbgEI9FDIsMGqbW0tisVJfhme0EBi1kZfTi4cIu9Pna5nYkNBefD/d/DuK0ZrRqdONNjhRkCO3TcVbtIQ==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/web/-/web-22.7.2.tgz", + "integrity": "sha512-DgjlnOlPOpRFHJuItUbm3+DRZqZQkqVUTRhxS/Ep5QtMx/KeO6jbULHFS4BTDV54/I20ejjsWvADYcYhsZaY1g==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "detect-port": "^1.5.1", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "detect-port": "^2.1.0", "http-server": "^14.1.0", "picocolors": "^1.1.0", "tslib": "^2.3.0" + }, + "peerDependencies": { + "@nx/cypress": "22.7.2", + "@nx/eslint": "22.7.2", + "@nx/jest": "22.7.2", + "@nx/playwright": "22.7.2", + "@nx/vite": "22.7.2", + "@nx/webpack": "22.7.2" + }, + "peerDependenciesMeta": { + "@nx/cypress": { + "optional": true + }, + "@nx/eslint": { + "optional": true + }, + "@nx/jest": { + "optional": true + }, + "@nx/playwright": { + "optional": true + }, + "@nx/vite": { + "optional": true + }, + "@nx/webpack": { + "optional": true + } } }, "node_modules/@nx/webpack": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-22.6.5.tgz", - "integrity": "sha512-LN75xxd/6U/r8vI3nzs/N5sj22nrJdBhTfDPlYlhKz2caCCWImSvQSXmprU46xNbXuYAY0DmRcZ5fkeqjHegtw==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-22.7.2.tgz", + "integrity": "sha512-XVJcf2Bn1P7GoxJRESKFF5bXqWGPyE6+Bw1BraUmtM7bBRiF3tSXaYrzdwQlyNxZdK82RH+Y8hkBSXX3VZ8Rkg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", - "@nx/devkit": "22.6.5", - "@nx/js": "22.6.5", - "@phenomnomnominal/tsquery": "~6.1.4", + "@nx/devkit": "22.7.2", + "@nx/js": "22.7.2", + "@phenomnomnominal/tsquery": "~6.2.0", "ajv": "^8.12.0", "autoprefixer": "^10.4.9", "babel-loader": "^9.1.2", @@ -10728,9 +10662,9 @@ } }, "node_modules/@nx/webpack/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -10812,17 +10746,17 @@ } }, "node_modules/@nx/workspace": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-22.6.5.tgz", - "integrity": "sha512-/CZtv1ESSfZ1MVqSlCsmnBWysU1z5VdNlwANlqL6BV2X6RUHKDPVj4YuNPvCK+0LsqyzfJdUt3pcnBYxnT5TIg==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-22.7.2.tgz", + "integrity": "sha512-xTEQMkeltIS6V5Qb6QRA7O+HIJQjIZSxLm6SvBNczJqAxckuYwMdbrb2IkDSE0XnQqR3gYg7Isz6UuBUHjz66Q==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.5", + "@nx/devkit": "22.7.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "22.6.5", + "nx": "22.7.2", "picomatch": "4.0.4", "semver": "^7.6.3", "tslib": "^2.3.0", @@ -10830,20 +10764,16 @@ } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-0.7.2.tgz", - "integrity": "sha512-Fry2mV7uGGJRmP9JntTZRc8ElESIk7AJNTacLbF6Syoeb5k8d7HPGkcK9rTXDlqBb8HgU1hOKtz23HojesTmnw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.9.0.tgz", + "integrity": "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ==", "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.8" - }, "engines": { "node": ">=18" }, "peerDependencies": { - "ai": "^4.3.16", - "zod": "^3.25.34" + "ai": "^6.0.0", + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/@opentelemetry/api": { @@ -11327,17 +11257,17 @@ } }, "node_modules/@phenomnomnominal/tsquery": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-6.1.4.tgz", - "integrity": "sha512-3tHlGy/fxjJCHqIV8nelAzbRTNkCUY+k7lqBGKNuQz99H2OKGRt6oU+U2SZs6LYrbOe8mxMFl6kq6gzHapFRkw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-6.2.0.tgz", + "integrity": "sha512-Vo9nkhfZxDB/sBiqIY3pjDC4mOSyure+AFlEW5hcy/tRE82MqCXjRN4InnVNMldinRt0dLYqg4HAU2XPq5e1LA==", "dev": true, "license": "MIT", "dependencies": { - "@types/esquery": "^1.5.0", - "esquery": "^1.5.0" + "@types/esquery": "^1.5.4", + "esquery": "^1.7.0" }, "peerDependencies": { - "typescript": "^3 || ^4 || ^5" + "typescript": ">3.0.0" } }, "node_modules/@pkgjs/parseargs": { @@ -11364,32 +11294,25 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, "node_modules/@prisma/adapter-pg": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.7.0.tgz", - "integrity": "sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", "license": "Apache-2.0", "dependencies": { - "@prisma/driver-adapter-utils": "7.7.0", + "@prisma/driver-adapter-utils": "7.8.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "node_modules/@prisma/client": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.7.0.tgz", - "integrity": "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", "license": "Apache-2.0", "dependencies": { - "@prisma/client-runtime-utils": "7.7.0" + "@prisma/client-runtime-utils": "7.8.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0" @@ -11408,28 +11331,28 @@ } }, "node_modules/@prisma/client-runtime-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.7.0.tgz", - "integrity": "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.7.0.tgz", - "integrity": "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "c12": "3.1.0", + "c12": "3.3.4", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz", - "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -11459,65 +11382,65 @@ } }, "node_modules/@prisma/driver-adapter-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.7.0.tgz", - "integrity": "sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.7.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/engines": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz", - "integrity": "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.7.0", - "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", - "@prisma/fetch-engine": "7.7.0", - "@prisma/get-platform": "7.7.0" + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz", - "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==", + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", - "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.7.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.7.0.tgz", - "integrity": "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.7.0", - "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", - "@prisma/get-platform": "7.7.0" + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", - "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.7.0" + "@prisma/debug": "7.8.0" } }, "node_modules/@prisma/get-platform": { @@ -11742,17 +11665,27 @@ } }, "node_modules/@redis/client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", - "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "cluster-key-slot": "1.1.2" }, "engines": { - "node": ">=14" + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@rolldown/binding-android-arm64": { @@ -12809,9 +12742,9 @@ } }, "node_modules/@rspack/dev-server/node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { @@ -12823,7 +12756,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -12882,15 +12815,15 @@ } }, "node_modules/@rspack/dev-server/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -12909,7 +12842,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -13107,22 +13040,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@rspack/dev-server/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@rspack/dev-server/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -14475,12 +14392,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/diff-match-patch": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", - "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", - "license": "MIT" - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -14791,9 +14702,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.7.tgz", - "integrity": "sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", "dev": true, "license": "MIT", "dependencies": { @@ -16006,6 +15917,15 @@ "d3-transition": "^3.0.1" } }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", @@ -16285,44 +16205,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz", - "integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@yarnpkg/parsers/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@zkochan/js-yaml": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", @@ -16430,13 +16312,13 @@ } }, "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz", + "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">= 16.0.0" } }, "node_modules/adjust-sourcemap-loader": { @@ -16498,29 +16380,21 @@ } }, "node_modules/ai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.16.tgz", - "integrity": "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==", + "version": "6.0.174", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.174.tgz", + "integrity": "sha512-bTrfLUWHWtkjzWyCY4bmyuk4Qvmj4S4NSNsXyNSVVqkmftQNtxRj7dzUoMeQDBBwlJO6fC7m2Q/lNOPqQQfAGA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "1.1.3", - "@ai-sdk/provider-utils": "2.2.8", - "@ai-sdk/react": "1.2.12", - "@ai-sdk/ui-utils": "1.2.11", - "@opentelemetry/api": "1.9.0", - "jsondiffpatch": "0.6.0" + "@ai-sdk/gateway": "3.0.109", + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.26", + "@opentelemetry/api": "1.9.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } + "zod": "^3.25.76 || ^4.1.8" } }, "node_modules/ajv": { @@ -16913,6 +16787,7 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -16996,13 +16871,13 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -17230,6 +17105,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -17629,27 +17505,27 @@ } }, "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", "devOptional": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -17657,22 +17533,6 @@ } } }, - "node_modules/c12/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/c12/node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -17681,9 +17541,9 @@ "license": "MIT" }, "node_modules/c12/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "devOptional": true, "license": "BSD-2-Clause", "engines": { @@ -17693,30 +17553,26 @@ "url": "https://dotenvx.com" } }, - "node_modules/c12/node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "node_modules/c12/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/c12/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/c12/node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "devOptional": true, "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" } }, "node_modules/cacache": { @@ -18251,16 +18107,6 @@ "node": ">=8" } }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -18976,9 +18822,9 @@ } }, "node_modules/countries-and-timezones": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.8.0.tgz", - "integrity": "sha512-+Ze9h5f4dQpUwbzTm0DEkiPiZyim9VHV4/mSnT4zNYJnrnfwsKjAZPtnp7J5VzejCDgySs+2SSc6MDdCnD43GA==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.9.0.tgz", + "integrity": "sha512-3upkVtUfZgdp5xOQI/Deg1ye73ae6bygJMO4UvtUi6wLjwcj7cBqx2PzSZ1VL8i5iveBbjhsQp2Judx7kPKivA==", "license": "MIT", "engines": { "node": ">=8.x", @@ -19042,48 +18888,6 @@ "node-fetch": "^2.7.0" } }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/cross-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/cross-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/cross-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -19194,13 +18998,13 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.7.tgz", - "integrity": "sha512-N5LGn/OlhMxDTvKACwUPMzT34SSj1b022pvUAE/Vh6r2WD1aUCbc+QNIP/JjX9VVxebdJWZQ3352Lt4oF7dQ/g==", + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz", + "integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==", "dev": true, "license": "MIT", "dependencies": { - "cssnano-preset-default": "^7.0.15", + "cssnano-preset-default": "^7.0.17", "lilconfig": "^3.1.3" }, "engines": { @@ -19211,71 +19015,71 @@ "url": "https://opencollective.com/cssnano" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-preset-default": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.15.tgz", - "integrity": "sha512-60kx7lJ40//HA85cIfQXSOJFby2D2V1pOMNHVCxue3KFWCjRzmiQyL9OvI+NAhwUlaojOfF9eK3nGvrJLCBUfQ==", + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.17.tgz", + "integrity": "sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA==", "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.28.2", "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.2", + "cssnano-utils": "^5.0.3", "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.9", - "postcss-convert-values": "^7.0.11", - "postcss-discard-comments": "^7.0.7", - "postcss-discard-duplicates": "^7.0.3", - "postcss-discard-empty": "^7.0.2", - "postcss-discard-overridden": "^7.0.2", - "postcss-merge-longhand": "^7.0.6", - "postcss-merge-rules": "^7.0.10", - "postcss-minify-font-values": "^7.0.2", - "postcss-minify-gradients": "^7.0.4", - "postcss-minify-params": "^7.0.8", - "postcss-minify-selectors": "^7.1.0", - "postcss-normalize-charset": "^7.0.2", - "postcss-normalize-display-values": "^7.0.2", - "postcss-normalize-positions": "^7.0.3", - "postcss-normalize-repeat-style": "^7.0.3", - "postcss-normalize-string": "^7.0.2", - "postcss-normalize-timing-functions": "^7.0.2", - "postcss-normalize-unicode": "^7.0.8", - "postcss-normalize-url": "^7.0.2", - "postcss-normalize-whitespace": "^7.0.2", - "postcss-ordered-values": "^7.0.3", - "postcss-reduce-initial": "^7.0.8", - "postcss-reduce-transforms": "^7.0.2", - "postcss-svgo": "^7.1.2", - "postcss-unique-selectors": "^7.0.6" + "postcss-colormin": "^7.0.10", + "postcss-convert-values": "^7.0.12", + "postcss-discard-comments": "^7.0.8", + "postcss-discard-duplicates": "^7.0.4", + "postcss-discard-empty": "^7.0.3", + "postcss-discard-overridden": "^7.0.3", + "postcss-merge-longhand": "^7.0.7", + "postcss-merge-rules": "^7.0.11", + "postcss-minify-font-values": "^7.0.3", + "postcss-minify-gradients": "^7.0.5", + "postcss-minify-params": "^7.0.9", + "postcss-minify-selectors": "^7.1.2", + "postcss-normalize-charset": "^7.0.3", + "postcss-normalize-display-values": "^7.0.3", + "postcss-normalize-positions": "^7.0.4", + "postcss-normalize-repeat-style": "^7.0.4", + "postcss-normalize-string": "^7.0.3", + "postcss-normalize-timing-functions": "^7.0.3", + "postcss-normalize-unicode": "^7.0.9", + "postcss-normalize-url": "^7.0.3", + "postcss-normalize-whitespace": "^7.0.3", + "postcss-ordered-values": "^7.0.4", + "postcss-reduce-initial": "^7.0.9", + "postcss-reduce-transforms": "^7.0.3", + "postcss-svgo": "^7.1.3", + "postcss-unique-selectors": "^7.0.7" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/cssnano-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.2.tgz", - "integrity": "sha512-kt41WLK7FLKfePzPi645Y+/NtW/nNM7Su6nlNUfJyRNW3JcuU3JU7+cWJc+JexTeZ8dRBvFufefdG2XpXkIo0A==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.3.tgz", + "integrity": "sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==", "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -19302,13 +19106,13 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-colormin": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.9.tgz", - "integrity": "sha512-EZpoUlmbXQUpe+g4ZaGM2kjGlHrQ7Bjzb3xHcNrC9ysI1tGoib6DAYvxg6aB7MGxsjgLF+Qx/jwZQkJ5cKDvXA==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.10.tgz", + "integrity": "sha512-yFr6JezOolHLta/buLE71VKPh2mXursp4saVe98/ol8ZnEWhL+racShqPKlvd/DKWLre/39B6HhcMXf7RZ3hxg==", "dev": true, "license": "MIT", "dependencies": { - "@colordx/core": "^5.2.0", + "@colordx/core": "^5.4.3", "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", "postcss-value-parser": "^4.2.0" @@ -19317,13 +19121,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-convert-values": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.11.tgz", - "integrity": "sha512-H+s7P0f9jJylSysAHs3/5MhAx7GthDO05uw1h56L2xyEqpiLTFLEqBNw3PUYzD5p/AKwWaigCXf6FGELpOw9lw==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.12.tgz", + "integrity": "sha512-xurKu5qqk4viR3Cp3p4xBR4KfnZm4w4ys6+UBwBmeuBSNkH7+DtLnYOYnOffgtE4yx8sH9S1VZ6RAAvROXzP2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19334,13 +19138,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-comments": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.7.tgz", - "integrity": "sha512-FJhE3fSte7HaRNL4iwD8LTG9vWqj3puxXIdig6LfrFqc1TJRUhY4kXOkeTXZZfTXYny+k+SO7fd2fymj1wduJg==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.8.tgz", + "integrity": "sha512-CvvS5S9WrXblFXCEJ9nVo+4z+eA7zSC7Z88V1HEJuwlQhlFnYTIjg1xJY+BCUiG2bvICap2tXii4mP22BD108Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19350,88 +19154,88 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-duplicates": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.3.tgz", - "integrity": "sha512-9cRxXwhEM/aNZon1qZyToX4NmjbFbxOGbww+0CnbYFDbbPRGZ8jg4IbM8UlA+CzkXxM35itxyaHKNqBBg/RTDg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.4.tgz", + "integrity": "sha512-VBNn1+EuMZkeGVVtz0gRfbNGtx9IFgAsAV+E2pHtXPrp4qfGBkhTIiAuE/wrb+Y6Pakg9NewAlfTpYIFAWODtw==", "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-empty": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.2.tgz", - "integrity": "sha512-NZFouOmOwtngJVgkNeI1LtkzFdYqIurxgy4wq3qNvIiXFURTZ3b/K7q3dP3QitlWQ5imHDQL0qSorItQhoxb1g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.3.tgz", + "integrity": "sha512-M2pyjQCU+/7cMHVtL6bKTHjv0lZnPLMpicgr67Dlth7AbuV9gjVTtUqaRwn6Pp6BwSDspUzhz8SaUrRykJU5Dw==", "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-discard-overridden": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.2.tgz", - "integrity": "sha512-Ym01X4v6U3sY8X0P1J9P+RTar+7JyLTOzDrxKSeaArFsLmkVu4KcAKPBWDYRIyZ/q4jwpSPnOnekeSSqXSXKUw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.3.tgz", + "integrity": "sha512-aNovXo9UsZuRNLzHJtp13lHIvinDPfiXBPePpXkSjCbgp++iU2FqE+YxvjIsg6EdyPZsASFbfu+JcBFVsErXIQ==", "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-longhand": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.6.tgz", - "integrity": "sha512-lDsWeKRsssX/9vKFpingoRiuvGajtOGCJhs1kyaTJ5fzaVzs0aPPYe38UZ/ukMFEA5iuRIjQJHIkH2niYO3ubQ==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.7.tgz", + "integrity": "sha512-b3mfYUxR388u5Pt0HPcVIUtUDn/k15UfTY9M+ORW+meCR6JLNxoZffiYvXyOYQoRYQNZyX/UFkMCM/mNHxe1qA==", "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.10" + "stylehacks": "^7.0.11" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-merge-rules": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.10.tgz", - "integrity": "sha512-UXYKxkg8Cy1so/evF7AE/25PNXZb3E0SrvjdbtbGf+MW+doLenKqRLQzz6YZW469ktiXK2MVLFWtel/DftCV0Q==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.11.tgz", + "integrity": "sha512-SJUPM18g2BmPhf8BVlbwqWz4aK3pLu6u6xjfwEzra7xL6IBR10sUaiB++EzqcVfadPHrKBSMlNdP+XieykhI+Q==", "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.2", + "cssnano-utils": "^5.0.3", "postcss-selector-parser": "^7.1.1" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-font-values": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.2.tgz", - "integrity": "sha512-Z82NUmnvhPrvMUaHfkaAVBmWQq9F8Dox4Dy0LiwbaTxfmDUWLQtS+0WCgKViwdWCPPajiY9YzoQftgqKdXkM5g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.3.tgz", + "integrity": "sha512-yilG/VOaNI74IylQvAQQxm3/wZVBkXyYUqNUAdxqwtbWUXPsbK1q8Ms0mL83v+f8YicgcyfYCRZtWACUdYajpA==", "dev": true, "license": "MIT", "dependencies": { @@ -19441,49 +19245,49 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-gradients": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.4.tgz", - "integrity": "sha512-g8MNeNyN+lbwKy8DCtJ6zU6awBL0InBsSOaKmgZ1MdRLVItLQUNFNAzzzBnOp4qowOcyyB6GetTlQ0/0UNXvag==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.5.tgz", + "integrity": "sha512-YraROyQRg3BI1+Hg8E05B/JPdnTm8EDSVu4P2BxdM+CRiOyfmou809+chGIqo6fQqwjPGQ947nbGncSjmTU1WQ==", "dev": true, "license": "MIT", "dependencies": { - "@colordx/core": "^5.2.0", - "cssnano-utils": "^5.0.2", + "@colordx/core": "^5.4.3", + "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-params": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.8.tgz", - "integrity": "sha512-DIUKM5DZGTmxN7KFKT+rxt0FdPDmRrdK/k3n3+6Po+N/QYn06juwagHcfOVBG0CfCHwcnI612GAUCZc3eT+ZEg==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.9.tgz", + "integrity": "sha512-R8itbB8BhlpoYyBm1ou0dD+vJnQ3F6adQipR4UnkCHUwlo+S9WXJaDRg1RHjC8YVAtIdrQzSWvJl40HnGDTKjA==", "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.28.2", - "cssnano-utils": "^5.0.2", + "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-minify-selectors": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.1.0.tgz", - "integrity": "sha512-HYl/6I0aL+UvpA10t65BSa7h+tVjBgE6oRI5N/3ylX3vtwvlDL67G3FT3vYDPnTksxr0riiyJcT0tBtyRVoloA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.1.2.tgz", + "integrity": "sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19496,26 +19300,26 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-charset": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.2.tgz", - "integrity": "sha512-YoINoiR4YKlzfB95Y93b0DSxWy7FLw+1SADIaznMHb88AKizpzfF80tolmiDEbYr1UM4r4Hw+NZq37SwT5f3uw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.3.tgz", + "integrity": "sha512-NoBfZu8PR4c2NlmjvrqQTzCzLY79hwcSRgNQ3ZiNK0ABzf9kYKloE/jNj+/8GQY1wsm8pRRgANk6ydLH8cwo0Q==", "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-display-values": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.2.tgz", - "integrity": "sha512-wu/NTSjnp8sX5TnEHVPN+eScjAtRs18ELtEduG+Ek3GxjeUDUT+VAA3PJjVIXBcVIk6fiLYFj2iKH0q99S3T2Q==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.3.tgz", + "integrity": "sha512-ldsCX0QIt05pKIOobZtVQ48wXJecr+czw4+e1/YjVhLMqslShgpVxgPtI2CefURR8oyVoYaU/l829MMwExDMLw==", "dev": true, "license": "MIT", "dependencies": { @@ -19525,13 +19329,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-positions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.3.tgz", - "integrity": "sha512-1CJI++oA3yK/fQlPUcEngUfcSWS08Pkt9fK+jVgL53mmtHDBHi0YiuB0m3D9BXwZjmfvCc2GQmFqCAF/CVcPzQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.4.tgz", + "integrity": "sha512-VEvlpeGd3Ju1Hqa/oN4jaP3+ms4laYwkEL9N9u+B6k54PZjXbW1n6wI+aVprf1BQXlCYpS5+1pl/7/vHiKgARg==", "dev": true, "license": "MIT", "dependencies": { @@ -19541,13 +19345,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-repeat-style": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.3.tgz", - "integrity": "sha512-RvImJ2Ml4LZSx31qC2C8LDiz65IgBNATtwEr9r3Aue+D0cCGbj4rjNojb/uGpEm4QxnOTzFqMvaDYuKiT1Cmpg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.4.tgz", + "integrity": "sha512-6mPKlY/8cSaDHxX502wERADarJsccwlky6yIrOapHH2ZgfoKAV94SbiTKfKEs4EEpdazuc3J72WsqeYk7hp9+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19557,13 +19361,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-string": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.2.tgz", - "integrity": "sha512-FqtrUh2BU2MnVeLeWBbJ2rwOjuDnA91XvoImc1BbgMWIxdxiPTaquflBHsmFBA3xh3pC3wPZO9W5MaIc7wU/Xw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.3.tgz", + "integrity": "sha512-HnEQPUchi1eznmDKEYrKUTqrprEq97SrpUYClgUkv7V2zRODD9DFoUsYU+m9ZOetmD5ku7fEMZB/lwy8IT6xVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19573,13 +19377,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-timing-functions": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.2.tgz", - "integrity": "sha512-5H5fpXBnMACEXzn7k9RP7qWZ1eWg8cuZkUuFygStY7icOj+UucwMWXeMmdkF/iITvTVa7fP85tdRCJeznpdFfQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.3.tgz", + "integrity": "sha512-zmEzHdvpZBZu0OKlbJSfgASQvaayyAoVuWtvyr34IJ/LyS+DaOKvvR3EvFJ9RWWtNIx+CMvO125OVophaxNYew==", "dev": true, "license": "MIT", "dependencies": { @@ -19589,13 +19393,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-unicode": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.8.tgz", - "integrity": "sha512-imCM3cwK3hvlAG4z1AzYM24m8BPA3/Jk/S71wfbn2I6+E2b+UwFaGvlNqydihXTSl3OFPeQXztqCzg+NGeSibQ==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.9.tgz", + "integrity": "sha512-DRAdWfeh/TjmhLJsw91vdiWCnUod9iwvM7xyS02/nF/sLsCR3A8l3pztrSUrWG8DSBqfX7yEk9FM0USaVJ2mSg==", "dev": true, "license": "MIT", "dependencies": { @@ -19606,13 +19410,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-url": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.2.tgz", - "integrity": "sha512-bLnNY7t76NLRb9QQyCVmCN4qwoHxiq6vABH/CXav9wTuR6dNGHGQ72AyO/+h2quWxZk3l7BqxNL1vtDi9H6y1g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.3.tgz", + "integrity": "sha512-CL93wmloq5qsffmFv+bw24MIRbmhHrp53qoh1LDAb/5TtjWEXI/np4xcP/Gw9oWCb2XyWnqHYLDUwiKRoJBA1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -19622,13 +19426,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-normalize-whitespace": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.2.tgz", - "integrity": "sha512-TNSVkuhkeOhl36WruQlflxOb7HweoeZowSusNpfsM1+ZvqJ24Mc+xksu05ecMQxlu+0zgI8pyznO2EWqDCQbLA==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.3.tgz", + "integrity": "sha512-FdHjjn+Ht5Z2ZRjNOmeCbNq6lq09sUYKpmlF/Aq0XjVNSLTL6fmHlA/3swN2wP2caY9GV/tjSDcIIyS7aN7W0A==", "dev": true, "license": "MIT", "dependencies": { @@ -19638,30 +19442,30 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-ordered-values": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.3.tgz", - "integrity": "sha512-FTt6R9RF7NAYfpOHa2XFPm89FVuo5GiIbcfwOXFy1MYF38BeiNW9ke8ybw9Pk62eEsUlRVVbxHWA3B7ERYqOOA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.4.tgz", + "integrity": "sha512-nubSi49hDHQk4E8KIj+IbLY8Bg+8OcSUEhgyolgM+atnOvXjV7EjaR6bac4YGZoFyPa9mWoAF3EaYbWdFkKqVg==", "dev": true, "license": "MIT", "dependencies": { - "cssnano-utils": "^5.0.2", + "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-initial": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.8.tgz", - "integrity": "sha512-VeVRmbgpgTZuRcDQdqnsB4iYTeS2dBRV07UdwK6V3x61F1xTQ2pgIzHBIR4rQYRlXRNKBTGYYhEL1eNA7w9vaQ==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.9.tgz", + "integrity": "sha512-ztTNPdIxXTxtBcG03E9u8v44M4ElXbMIRT7pf2onlquGula0Y83nKKxqM22FA/hMgkfCjN7ohevkVlaNwI8iOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19672,13 +19476,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-reduce-transforms": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.2.tgz", - "integrity": "sha512-OV5P9hMnf7kEkeXVXyS5ESqxbIls7a3TqFymUAV5JICO/9YCBEU+QQhQjZiDHaLwFdV7/CL481kVeBUk5FdY3w==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.3.tgz", + "integrity": "sha512-FXsnN9ZwcZTT8Yf8cAHA8qIGUXcX6WfLd9JoYhrdDfmvsVhhfqkkv7m4AC3rwFOfz+GzkUa87OCKF9dUcicd+g==", "dev": true, "license": "MIT", "dependencies": { @@ -19688,13 +19492,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-svgo": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.2.tgz", - "integrity": "sha512-ixExc8m+/68yuSYQzV/1DgtTup/7nI2dN9eiDS5GMRUzeCH4q9UcqeZPwcSVhdf8ay9fRwXDUHwcY5/XzQSszQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.3.tgz", + "integrity": "sha512-2QfoFOYMcj8lwcVEf9WeTlkVIAm7u2QvOEhMzkQU3KUhhGX/l8hVV9EtjMv4iq3E9iI3OeeMN0YoMLbGusuigw==", "dev": true, "license": "MIT", "dependencies": { @@ -19705,13 +19509,13 @@ "node": "^18.12.0 || ^20.9.0 || >= 18" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss-unique-selectors": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.6.tgz", - "integrity": "sha512-cDxnYw1QuBMW5w3svZ0BlYF0IA4Amr+1JoTLXzu6vDFPNwohN2QU+sPZNx15b930LR7ce+/600h28/cYoxO9vw==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.7.tgz", + "integrity": "sha512-d+sCkaRnSefghOUdH8CMJZV9yUQhj2ojpe8Nw/lA+LV1UOfeleGkLTl6XdCFFSai9UJ+DJPb69FFuqthXYsY8w==", "dev": true, "license": "MIT", "dependencies": { @@ -19721,13 +19525,13 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/stylehacks": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.10.tgz", - "integrity": "sha512-sRJ7klmhe/Fl5woJcbJUa2qP1Ueffsl1CQI4ePvqXLkZmcIuAt09aP9uT/FOFPqXh9Rh8M5UkgEnwTdTKn/Aag==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.11.tgz", + "integrity": "sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg==", "dev": true, "license": "MIT", "dependencies": { @@ -19738,7 +19542,7 @@ "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "peerDependencies": { - "postcss": "^8.5.10" + "postcss": "^8.5.13" } }, "node_modules/css-select": { @@ -20605,18 +20409,6 @@ "lodash-es": "^4.17.21" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -20715,13 +20507,6 @@ "optional": true, "peer": true }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -20954,7 +20739,9 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -21005,21 +20792,20 @@ "license": "MIT" }, "node_modules/detect-port": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", - "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz", + "integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==", "dev": true, "license": "MIT", "dependencies": { - "address": "^1.0.1", - "debug": "4" + "address": "^2.0.1" }, "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" + "detect": "dist/commonjs/bin/detect-port.js", + "detect-port": "dist/commonjs/bin/detect-port.js" }, "engines": { - "node": ">= 4.0.0" + "node": ">= 16.0.0" } }, "node_modules/diff": { @@ -21032,12 +20818,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", - "license": "Apache-2.0" - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -21212,13 +20992,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -21256,6 +21029,7 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" @@ -22246,10 +22020,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "dev": true, + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -22595,32 +22368,6 @@ } } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/fetch-mock-cache": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/fetch-mock-cache/-/fetch-mock-cache-2.3.1.tgz", @@ -22692,6 +22439,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" @@ -22701,6 +22449,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -22710,6 +22459,7 @@ "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -23000,9 +22750,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -23245,21 +22995,6 @@ "node": ">= 0.6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -23292,40 +23027,6 @@ "node": ">= 0.8" } }, - "node_modules/front-matter": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", - "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1" - } - }, - "node_modules/front-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/front-matter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -23415,12 +23116,16 @@ } }, "node_modules/fuse.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", - "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", "license": "Apache-2.0", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" } }, "node_modules/gaxios": { @@ -23464,48 +23169,6 @@ "node": ">= 6" } }, - "node_modules/gaxios/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gaxios/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/gaxios/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/gaxios/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/gcp-metadata": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", @@ -23539,15 +23202,6 @@ "node": ">= 0.4" } }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -23683,19 +23337,11 @@ "license": "MIT" }, "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", "devOptional": true, "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, "bin": { "giget": "dist/cli.mjs" } @@ -24001,22 +23647,6 @@ "node": ">=10" } }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/hachure-fill": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", @@ -24190,8 +23820,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/hosted-git-info": { "version": "9.0.2", @@ -25713,6 +25342,7 @@ "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "async": "^3.2.6", @@ -29086,35 +28716,6 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, - "node_modules/jsondiffpatch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", - "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", - "license": "MIT", - "dependencies": { - "@types/diff-match-patch": "^1.0.36", - "chalk": "^5.3.0", - "diff-match-patch": "^1.0.5" - }, - "bin": { - "jsondiffpatch": "bin/jsondiffpatch.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/jsondiffpatch/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -29945,6 +29546,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "devOptional": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -30916,29 +30518,6 @@ "license": "MIT", "optional": true }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -30969,33 +30548,47 @@ } }, "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", @@ -31269,65 +30862,139 @@ "license": "MIT" }, "node_modules/nx": { - "version": "22.6.5", - "resolved": "https://registry.npmjs.org/nx/-/nx-22.6.5.tgz", - "integrity": "sha512-VRKhDAt684dXNSz9MNjE7MekkCfQF41P2PSx5jEWQjDEP1Z4jFZbyeygWs5ZyOroG7/n0MoWAJTe6ftvIcBOAg==", + "version": "22.7.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-22.7.2.tgz", + "integrity": "sha512-Gh7gGO1t/TvgbKuVJMYWbxUwZC+E+PuRRVUeoOeVe82yEvBNl40EKiVHIbbi6GID0s9Zwzflo07UrKGLoDSVGw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { + "@emnapi/core": "1.4.5", + "@emnapi/runtime": "1.4.5", + "@emnapi/wasi-threads": "1.0.4", + "@jest/diff-sequences": "30.0.1", "@napi-rs/wasm-runtime": "0.2.4", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.2", + "@tybys/wasm-util": "0.9.0", + "@yarnpkg/lockfile": "1.1.0", "@zkochan/js-yaml": "0.0.7", - "axios": "1.15.0", + "ansi-colors": "4.1.3", + "ansi-regex": "5.0.1", + "ansi-styles": "4.3.0", + "argparse": "2.0.1", + "asynckit": "0.4.0", + "axios": "1.16.0", + "balanced-match": "4.0.3", + "base64-js": "1.5.1", + "bl": "4.1.0", + "brace-expansion": "5.0.5", + "buffer": "5.7.1", + "call-bind-apply-helpers": "1.0.2", + "chalk": "4.1.2", "cli-cursor": "3.1.0", "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", + "cliui": "8.0.1", + "clone": "1.0.4", + "color-convert": "2.0.1", + "color-name": "1.1.4", + "combined-stream": "1.0.8", + "defaults": "1.0.4", + "define-lazy-prop": "2.0.0", + "delayed-stream": "1.0.0", + "dotenv": "16.4.7", + "dotenv-expand": "12.0.3", + "dunder-proto": "1.0.1", "ejs": "5.0.1", - "enquirer": "~2.3.6", + "emoji-regex": "8.0.0", + "end-of-stream": "1.4.5", + "enquirer": "2.3.6", + "es-define-property": "1.0.1", + "es-errors": "1.3.0", + "es-object-atoms": "1.1.1", + "es-set-tostringtag": "2.1.0", + "escalade": "3.2.0", + "escape-string-regexp": "1.0.5", "figures": "3.2.0", - "flat": "^5.0.2", - "front-matter": "^4.0.2", - "ignore": "^7.0.5", - "jest-diff": "^30.0.2", + "flat": "5.0.2", + "follow-redirects": "1.16.0", + "form-data": "4.0.5", + "fs-constants": "1.0.0", + "function-bind": "1.1.2", + "get-caller-file": "2.0.5", + "get-intrinsic": "1.3.0", + "get-proto": "1.0.1", + "gopd": "1.2.0", + "has-flag": "4.0.0", + "has-symbols": "1.1.0", + "has-tostringtag": "1.0.2", + "hasown": "2.0.2", + "ieee754": "1.2.1", + "ignore": "7.0.5", + "inherits": "2.0.4", + "is-docker": "2.2.1", + "is-fullwidth-code-point": "3.0.0", + "is-interactive": "1.0.0", + "is-unicode-supported": "0.1.0", + "is-wsl": "2.2.0", + "json5": "2.2.3", "jsonc-parser": "3.2.0", "lines-and-columns": "2.0.3", - "minimatch": "10.2.4", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", + "log-symbols": "4.1.0", + "math-intrinsics": "1.1.0", + "mime-db": "1.52.0", + "mime-types": "2.1.35", + "mimic-fn": "2.1.0", + "minimatch": "10.2.5", + "minimist": "1.2.8", + "npm-run-path": "4.0.1", + "once": "1.4.0", + "onetime": "5.1.2", + "open": "8.4.2", "ora": "5.3.0", - "picocolors": "^1.1.0", + "path-key": "3.1.1", + "picocolors": "1.1.1", + "proxy-from-env": "2.1.0", + "readable-stream": "3.6.2", + "require-directory": "2.1.1", "resolve.exports": "2.0.3", - "semver": "^7.6.3", + "restore-cursor": "3.1.0", + "safe-buffer": "5.2.1", + "semver": "7.7.4", + "signal-exit": "3.0.7", "smol-toml": "1.6.1", - "string-width": "^4.2.3", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tree-kill": "^1.2.2", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yaml": "^2.6.0", - "yargs": "^17.6.2", + "string_decoder": "1.3.0", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-bom": "3.0.0", + "supports-color": "7.2.0", + "tar-stream": "2.2.0", + "tmp": "0.2.4", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tslib": "2.8.1", + "util-deprecate": "1.0.2", + "wcwidth": "1.0.1", + "wrap-ansi": "7.0.0", + "wrappy": "1.0.2", + "y18n": "5.0.8", + "yaml": "2.8.0", + "yargs": "17.7.2", "yargs-parser": "21.1.1" }, "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" + "nx": "dist/bin/nx.js", + "nx-cloud": "dist/bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "22.6.5", - "@nx/nx-darwin-x64": "22.6.5", - "@nx/nx-freebsd-x64": "22.6.5", - "@nx/nx-linux-arm-gnueabihf": "22.6.5", - "@nx/nx-linux-arm64-gnu": "22.6.5", - "@nx/nx-linux-arm64-musl": "22.6.5", - "@nx/nx-linux-x64-gnu": "22.6.5", - "@nx/nx-linux-x64-musl": "22.6.5", - "@nx/nx-win32-arm64-msvc": "22.6.5", - "@nx/nx-win32-x64-msvc": "22.6.5" + "@nx/nx-darwin-arm64": "22.7.2", + "@nx/nx-darwin-x64": "22.7.2", + "@nx/nx-freebsd-x64": "22.7.2", + "@nx/nx-linux-arm-gnueabihf": "22.7.2", + "@nx/nx-linux-arm64-gnu": "22.7.2", + "@nx/nx-linux-arm64-musl": "22.7.2", + "@nx/nx-linux-x64-gnu": "22.7.2", + "@nx/nx-linux-x64-musl": "22.7.2", + "@nx/nx-win32-arm64-msvc": "22.7.2", + "@nx/nx-win32-x64-msvc": "22.7.2" }, "peerDependencies": { "@swc-node/register": "^1.11.1", @@ -31342,14 +31009,65 @@ } } }, + "node_modules/nx/node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/nx/node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/nx/node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/nx/node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/nx/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/nx/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/nx/node_modules/brace-expansion": { @@ -31401,22 +31119,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/nx/node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/nx/node_modules/ejs": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", @@ -31437,6 +31139,16 @@ "dev": true, "license": "MIT" }, + "node_modules/nx/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/nx/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -31533,14 +31245,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nx/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nx/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nx/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -31651,6 +31386,37 @@ "node": ">=6" } }, + "node_modules/nx/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/nx/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/nx/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -31670,31 +31436,6 @@ "node": ">=12" } }, - "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.2.0", - "pathe": "^2.0.3", - "tinyexec": "^1.0.2" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", - "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "devOptional": true, - "license": "MIT" - }, "node_modules/oauth": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", @@ -32183,9 +31924,9 @@ } }, "node_modules/papaparse": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.1.tgz", - "integrity": "sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", "license": "MIT" }, "node_modules/param-case": { @@ -32644,9 +32385,9 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "devOptional": true, "license": "MIT" }, @@ -33380,16 +33121,16 @@ } }, "node_modules/prisma": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz", - "integrity": "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.7.0", + "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", - "@prisma/engines": "7.7.0", + "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" @@ -33667,20 +33408,21 @@ } }, "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", "devOptional": true, "license": "MIT", "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" + "defu": "^6.1.6", + "destr": "^2.0.5" } }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -35384,12 +35126,6 @@ "dev": true, "license": "MIT" }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause" - }, "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -36006,21 +35742,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -36885,19 +36606,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/swr": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", - "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -37301,18 +37009,6 @@ "tslib": "^2" } }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -37339,8 +37035,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -37404,9 +37101,9 @@ "peer": true }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", "dev": true, "license": "MIT", "engines": { @@ -37460,16 +37157,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -38701,6 +38388,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -39015,18 +38703,6 @@ "license": "MIT", "optional": true }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -39087,75 +38763,6 @@ } } }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-dev-middleware": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", @@ -40336,8 +39943,9 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -40548,6 +40156,7 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" diff --git a/package.json b/package.json index 54c1f048f..88750699e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.2.0", + "version": "3.5.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -12,7 +12,6 @@ "affected:libs": "nx affected:libs", "affected:lint": "nx affected:lint", "affected:test": "nx affected:test", - "analyze:client": "nx run client:build:production --stats-json && webpack-bundle-analyzer -p 1234 dist/apps/client/en/stats.json", "angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng", "build:production": "nx run api:copy-assets && nx run api:build:production && nx run client:copy-assets && nx run client:build:production && nx run ui:build-storybook && npm run replace-placeholders-in-build", "build:storybook": "nx run ui:build-storybook", @@ -66,14 +65,14 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "6.20.3", - "@bull-board/express": "6.20.3", - "@bull-board/nestjs": "6.20.3", + "@bull-board/api": "7.1.5", + "@bull-board/express": "7.1.5", + "@bull-board/nestjs": "7.1.5", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", - "@internationalized/number": "3.6.5", - "@ionic/angular": "8.8.1", - "@keyv/redis": "4.4.0", + "@internationalized/number": "3.6.6", + "@ionic/angular": "8.8.5", + "@keyv/redis": "5.1.6", "@nestjs/bull": "11.0.4", "@nestjs/cache-manager": "3.1.0", "@nestjs/common": "11.1.19", @@ -85,12 +84,12 @@ "@nestjs/platform-express": "11.1.19", "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", - "@openrouter/ai-sdk-provider": "0.7.2", - "@prisma/adapter-pg": "7.7.0", - "@prisma/client": "7.7.0", + "@openrouter/ai-sdk-provider": "2.9.0", + "@prisma/adapter-pg": "7.8.0", + "@prisma/client": "7.8.0", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "ai": "4.3.16", + "ai": "6.0.174", "alphavantage": "2.2.0", "big.js": "7.0.1", "bootstrap": "4.6.2", @@ -105,7 +104,7 @@ "class-validator": "0.15.1", "color": "5.0.3", "cookie-parser": "1.4.7", - "countries-and-timezones": "3.8.0", + "countries-and-timezones": "3.9.0", "countries-list": "3.3.0", "countup.js": "2.10.0", "date-fns": "4.1.0", @@ -113,7 +112,7 @@ "dotenv-expand": "12.0.3", "envalid": "8.1.1", "fast-redact": "3.5.0", - "fuse.js": "7.1.0", + "fuse.js": "7.3.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", "http-status-codes": "2.3.0", @@ -127,7 +126,7 @@ "ngx-markdown": "21.2.0", "ngx-skeleton-loader": "12.0.0", "open-color": "1.9.1", - "papaparse": "5.3.1", + "papaparse": "5.5.3", "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-headerapikey": "1.2.2", @@ -158,16 +157,16 @@ "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", "@nestjs/testing": "11.1.19", - "@nx/angular": "22.6.5", - "@nx/eslint-plugin": "22.6.5", - "@nx/jest": "22.6.5", - "@nx/js": "22.6.5", - "@nx/module-federation": "22.6.5", - "@nx/nest": "22.6.5", - "@nx/node": "22.6.5", - "@nx/storybook": "22.6.5", - "@nx/web": "22.6.5", - "@nx/workspace": "22.6.5", + "@nx/angular": "22.7.2", + "@nx/eslint-plugin": "22.7.2", + "@nx/jest": "22.7.2", + "@nx/js": "22.7.2", + "@nx/module-federation": "22.7.2", + "@nx/nest": "22.7.2", + "@nx/node": "22.7.2", + "@nx/storybook": "22.7.2", + "@nx/web": "22.7.2", + "@nx/workspace": "22.7.2", "@schematics/angular": "21.2.6", "@storybook/addon-docs": "10.1.10", "@storybook/addon-themes": "10.1.10", @@ -181,7 +180,7 @@ "@types/jsonpath": "0.2.4", "@types/lodash": "4.17.24", "@types/node": "22.15.17", - "@types/papaparse": "5.3.7", + "@types/papaparse": "5.5.2", "@types/passport-google-oauth20": "2.0.17", "@types/passport-openidconnect": "0.1.3", "@typescript-eslint/eslint-plugin": "8.43.0", @@ -194,10 +193,10 @@ "jest": "30.2.0", "jest-environment-jsdom": "30.2.0", "jest-preset-angular": "16.0.0", - "nx": "22.6.5", + "nx": "22.7.2", "prettier": "3.8.3", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.7.0", + "prisma": "7.8.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "8.4.0", @@ -206,8 +205,7 @@ "ts-jest": "29.4.0", "ts-node": "10.9.2", "tslib": "2.8.1", - "typescript": "5.9.2", - "webpack-bundle-analyzer": "4.10.2" + "typescript": "5.9.2" }, "engines": { "node": ">=22.18.0" diff --git a/prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql b/prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql new file mode 100644 index 000000000..f5c8ae016 --- /dev/null +++ b/prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Subscription" ADD COLUMN "stripeCheckoutSessionId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "Subscription_stripeCheckoutSessionId_key" ON "Subscription"("stripeCheckoutSessionId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a739ce8ff..024ab7aa0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -236,13 +236,14 @@ model SymbolProfileOverrides { } model Subscription { - createdAt DateTime @default(now()) - expiresAt DateTime - id String @id @default(uuid()) - price Float? - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], onDelete: Cascade, references: [id]) - userId String + createdAt DateTime @default(now()) + expiresAt DateTime + id String @id @default(uuid()) + price Float? + stripeCheckoutSessionId String? @unique + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], onDelete: Cascade, references: [id]) + userId String @@index([userId]) } diff --git a/prisma/seed.mts b/prisma/seed.mts index 4819e241e..81f34d05a 100644 --- a/prisma/seed.mts +++ b/prisma/seed.mts @@ -2,7 +2,7 @@ import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; const adapter = new PrismaPg({ - connectionString: process.env.DATABASE_URL + connectionString: process.env.DIRECT_URL ?? process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); diff --git a/skills-lock.json b/skills-lock.json index a15c56b59..0ecf51f96 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -6,6 +6,12 @@ "sourceType": "github", "skillPath": "angular-developer/SKILL.md", "computedHash": "28eb592b92e5a24c4e3a1c0229a854069f0b8c49bed7b8d2bf6b852812dbe214" + }, + "nestjs-best-practices": { + "source": "kadajett/agent-nestjs-skills", + "sourceType": "github", + "skillPath": "SKILL.md", + "computedHash": "1b6f82e889d19d305e38e35594de08eca0242321f353cafa4cf5e61dd3aa1a73" } } }