Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 34m46s

This commit is contained in:
sudacode 2025-03-18 20:08:39 -07:00
commit ee361bf669
9 changed files with 66 additions and 25 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for filtering in the _Copy AI prompt to clipboard_ actions on the analysis page (experimental)
### Changed ### Changed
- Improved the symbol validation in the _Yahoo Finance_ service (get asset profiles) - Improved the symbol validation in the _Yahoo Finance_ service (get asset profiles)
@ -15,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue in the activities import functionality related to the account balances - Fixed an issue in the activities import functionality related to the account balances
- Changed client-side dates to be sent in UTC format to ensure date consistency
- Benchmark endpoint
- Exchange rate endpoint
## 2.146.0 - 2025-03-15 ## 2.146.0 - 2025-03-15

View File

@ -1,5 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_LANGUAGE_CODE DEFAULT_LANGUAGE_CODE
@ -8,7 +9,14 @@ import { AiPromptResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types'; import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Param, UseGuards } from '@nestjs/common'; import {
Controller,
Get,
Inject,
Param,
Query,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -18,6 +26,7 @@ import { AiService } from './ai.service';
export class AiController { export class AiController {
public constructor( public constructor(
private readonly aiService: AiService, private readonly aiService: AiService,
private readonly apiService: ApiService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -25,9 +34,23 @@ export class AiController {
@HasPermission(permissions.readAiPrompt) @HasPermission(permissions.readAiPrompt)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPrompt( public async getPrompt(
@Param('mode') mode: AiPromptMode @Param('mode') mode: AiPromptMode,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise<AiPromptResponse> { ): Promise<AiPromptResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
});
const prompt = await this.aiService.getPrompt({ const prompt = await this.aiService.getPrompt({
filters,
mode, mode,
impersonationId: undefined, impersonationId: undefined,
languageCode: languageCode:

View File

@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
@ -25,6 +26,7 @@ import { AiService } from './ai.service';
@Module({ @Module({
controllers: [AiController], controllers: [AiController],
imports: [ imports: [
ApiModule,
ConfigurationModule, ConfigurationModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,

View File

@ -1,4 +1,5 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { Filter } from '@ghostfolio/common/interfaces';
import type { AiPromptMode } from '@ghostfolio/common/types'; import type { AiPromptMode } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -8,12 +9,14 @@ export class AiService {
public constructor(private readonly portfolioService: PortfolioService) {} public constructor(private readonly portfolioService: PortfolioService) {}
public async getPrompt({ public async getPrompt({
filters,
impersonationId, impersonationId,
languageCode, languageCode,
mode, mode,
userCurrency, userCurrency,
userId userId
}: { }: {
filters?: Filter[];
impersonationId: string; impersonationId: string;
languageCode: string; languageCode: string;
mode: AiPromptMode; mode: AiPromptMode;
@ -21,6 +24,7 @@ export class AiService {
userId: string; userId: string;
}) { }) {
const { holdings } = await this.portfolioService.getDetails({ const { holdings } = await this.portfolioService.getDetails({
filters,
impersonationId, impersonationId,
userId userId
}); });

View File

@ -190,7 +190,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
} }
this.dataService this.dataService
.fetchPrompt(mode) .fetchPrompt({
mode,
filters: this.userService.getFilters()
})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ prompt }) => { .subscribe(({ prompt }) => {
this.clipboard.copy(prompt); this.clipboard.copy(prompt);

View File

@ -8,7 +8,6 @@ import {
PROPERTY_API_KEY_GHOSTFOLIO PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
AdminData, AdminData,
@ -25,7 +24,6 @@ import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { DataSource, MarketData, Platform } from '@prisma/client'; import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { format } from 'date-fns';
import { switchMap } from 'rxjs'; import { switchMap } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@ -186,19 +184,8 @@ export class AdminService {
); );
} }
public gatherSymbol({ public gatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
dataSource, const url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
date,
symbol
}: AssetProfileIdentifier & {
date?: Date;
}) {
let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
if (date) {
url = `${url}/${format(date, DATE_FORMAT)}`;
}
return this.http.post<MarketData | void>(url, {}); return this.http.post<MarketData | void>(url, {});
} }

View File

@ -57,6 +57,7 @@ import { translate } from '@ghostfolio/ui/i18n';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { utc } from '@date-fns/utc';
import { import {
AccountBalance, AccountBalance,
DataSource, DataSource,
@ -281,7 +282,7 @@ export class DataService {
symbol: string; symbol: string;
}) { }) {
return this.http.get<IDataProviderHistoricalResponse>( return this.http.get<IDataProviderHistoricalResponse>(
`/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT)}` `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}`
); );
} }
@ -363,10 +364,7 @@ export class DataService {
} }
return this.http.get<BenchmarkMarketDataDetails>( return this.http.get<BenchmarkMarketDataDetails>(
`/api/v1/benchmarks/${dataSource}/${symbol}/${format( `/api/v1/benchmarks/${dataSource}/${symbol}/${format(startDate, DATE_FORMAT, { in: utc })}`,
startDate,
DATE_FORMAT
)}`,
{ params } { params }
); );
} }
@ -655,8 +653,18 @@ export class DataService {
return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report'); return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
} }
public fetchPrompt(mode: AiPromptMode) { public fetchPrompt({
return this.http.get<AiPromptResponse>(`/api/v1/ai/prompt/${mode}`); filters,
mode
}: {
filters?: Filter[];
mode: AiPromptMode;
}) {
const params = this.buildFiltersAsQueryParams({ filters });
return this.http.get<AiPromptResponse>(`/api/v1/ai/prompt/${mode}`, {
params
});
} }
public fetchPublicPortfolio(aAccessId: string) { public fetchPublicPortfolio(aAccessId: string) {

6
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@angular/router": "19.2.1", "@angular/router": "19.2.1",
"@angular/service-worker": "19.2.1", "@angular/service-worker": "19.2.1",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0",
"@dfinity/agent": "0.15.7", "@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7", "@dfinity/auth-client": "0.15.7",
"@dfinity/candid": "0.15.7", "@dfinity/candid": "0.15.7",
@ -3113,6 +3114,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@date-fns/utc": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@date-fns/utc/-/utc-2.1.0.tgz",
"integrity": "sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA=="
},
"node_modules/@dfinity/agent": { "node_modules/@dfinity/agent": {
"version": "0.15.7", "version": "0.15.7",
"resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.15.7.tgz", "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.15.7.tgz",

View File

@ -68,6 +68,7 @@
"@angular/router": "19.2.1", "@angular/router": "19.2.1",
"@angular/service-worker": "19.2.1", "@angular/service-worker": "19.2.1",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.0",
"@dfinity/agent": "0.15.7", "@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7", "@dfinity/auth-client": "0.15.7",
"@dfinity/candid": "0.15.7", "@dfinity/candid": "0.15.7",