From 6a195a7a361a0466808f145ee351bf6d76c2cc0b Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Oct 2024 20:03:10 +0100
Subject: [PATCH 01/65] Feature/extend sitemap.xml by resources sub pages in de
(#3993)
* Extend sitemap.xml
---
apps/api/src/assets/sitemap.xml | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml
index 17a6bc0f..3a0f44ff 100644
--- a/apps/api/src/assets/sitemap.xml
+++ b/apps/api/src/assets/sitemap.xml
@@ -56,10 +56,22 @@
https://ghostfol.io/de/ressourcen
${currentDate}T00:00:00+00:00
+
+ https://ghostfol.io/de/ressourcen/lexikon
+ ${currentDate}T00:00:00+00:00
+
+
+ https://ghostfol.io/de/ressourcen/maerkte
+ ${currentDate}T00:00:00+00:00
+
https://ghostfol.io/de/ressourcen/personal-finance-tools
${currentDate}T00:00:00+00:00
+
+ https://ghostfol.io/de/ressourcen/ratgeber
+ ${currentDate}T00:00:00+00:00
+
https://ghostfol.io/de/ueber-uns
${currentDate}T00:00:00+00:00
From 5de176a7ef829bc441bd7998638bb50981cd677e Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Oct 2024 20:03:37 +0100
Subject: [PATCH 02/65] Feature/rename allocation cluster risk x ray rule
(#3994)
* Rename Allocation Cluster Risk to Economic Market Cluster Risk
* Update changelog
---
CHANGELOG.md | 9 +++++----
apps/api/src/app/portfolio/portfolio.service.ts | 10 +++++-----
apps/api/src/app/user/user.service.ts | 12 ++++++------
.../developed-markets.ts | 4 ++--
.../emerging-markets.ts | 4 ++--
.../app/pages/portfolio/fire/fire-page.component.ts | 10 +++++-----
.../src/app/pages/portfolio/fire/fire-page.html | 4 ++--
.../lib/interfaces/x-ray-rules-settings.interface.ts | 4 ++--
8 files changed, 29 insertions(+), 28 deletions(-)
rename apps/api/src/models/rules/{allocation-cluster-risk => economic-market-cluster-risk}/developed-markets.ts (95%)
rename apps/api/src/models/rules/{allocation-cluster-risk => economic-market-cluster-risk}/emerging-markets.ts (95%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f419ecfa..0dcbcc74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Restructured the resources page
+- Renamed the static portfolio analysis rule from _Allocation Cluster Risk_ to _Economic Market Cluster Risk_ (Developed Markets and Emerging Markets)
- Improved the language localization for German (`de`)
- Switched the `consistent-generic-constructors` rule from `warn` to `error` in the `eslint` configuration
- Switched the `consistent-type-assertions` rule from `warn` to `error` in the `eslint` configuration
@@ -29,15 +30,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
- Fixed an issue with the X-axis scale of the portfolio evolution chart on the analysis page
-- Fixed an issue in the calculation of the static portfolio analysis rule: Allocation Cluster Risk (Developed Markets)
-- Fixed an issue in the calculation of the static portfolio analysis rule: Allocation Cluster Risk (Emerging Markets)
+- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Developed Markets)
+- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Emerging Markets)
## 2.118.0 - 2024-10-23
### Added
-- Added a new static portfolio analysis rule: Allocation Cluster Risk (Developed Markets)
-- Added a new static portfolio analysis rule: Allocation Cluster Risk (Emerging Markets)
+- Added a new static portfolio analysis rule: _Allocation Cluster Risk_ (Developed Markets)
+- Added a new static portfolio analysis rule: _Allocation Cluster Risk_ (Emerging Markets)
- Added support for mutual funds in the _EOD Historical Data_ service
### Changed
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index d88d925a..d4018686 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -7,10 +7,10 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
-import { AllocationClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/developed-markets';
-import { AllocationClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/emerging-markets';
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
+import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets';
+import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
@@ -1193,16 +1193,16 @@ export class PortfolioService {
userSettings
)
: undefined,
- allocationClusterRisk:
+ economicMarketClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
- new AllocationClusterRiskDevelopedMarkets(
+ new EconomicMarketClusterRiskDevelopedMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.developedMarkets.valueInBaseCurrency
),
- new AllocationClusterRiskEmergingMarkets(
+ new EconomicMarketClusterRiskEmergingMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.emergingMarkets.valueInBaseCurrency
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 556d2834..288e2aba 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -4,10 +4,10 @@ import { environment } from '@ghostfolio/api/environments/environment';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
-import { AllocationClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/developed-markets';
-import { AllocationClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/emerging-markets';
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
+import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets';
+import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
@@ -217,14 +217,14 @@ export class UserService {
undefined,
{}
).getSettings(user.Settings.settings),
- AllocationClusterRiskDevelopedMarkets:
- new AllocationClusterRiskDevelopedMarkets(
+ EconomicMarketClusterRiskDevelopedMarkets:
+ new EconomicMarketClusterRiskDevelopedMarkets(
undefined,
undefined,
undefined
).getSettings(user.Settings.settings),
- AllocationClusterRiskEmergingMarkets:
- new AllocationClusterRiskEmergingMarkets(
+ EconomicMarketClusterRiskEmergingMarkets:
+ new EconomicMarketClusterRiskEmergingMarkets(
undefined,
undefined,
undefined
diff --git a/apps/api/src/models/rules/allocation-cluster-risk/developed-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
similarity index 95%
rename from apps/api/src/models/rules/allocation-cluster-risk/developed-markets.ts
rename to apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
index 068ebdda..15e11392 100644
--- a/apps/api/src/models/rules/allocation-cluster-risk/developed-markets.ts
+++ b/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
@@ -3,7 +3,7 @@ import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
-export class AllocationClusterRiskDevelopedMarkets extends Rule {
+export class EconomicMarketClusterRiskDevelopedMarkets extends Rule {
private currentValueInBaseCurrency: number;
private developedMarketsValueInBaseCurrency: number;
@@ -13,7 +13,7 @@ export class AllocationClusterRiskDevelopedMarkets extends Rule {
developedMarketsValueInBaseCurrency: number
) {
super(exchangeRateDataService, {
- key: AllocationClusterRiskDevelopedMarkets.name,
+ key: EconomicMarketClusterRiskDevelopedMarkets.name,
name: 'Developed Markets'
});
diff --git a/apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
similarity index 95%
rename from apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts
rename to apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
index e7c10751..8fccdf1d 100644
--- a/apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts
+++ b/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
@@ -3,7 +3,7 @@ import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { UserSettings } from '@ghostfolio/common/interfaces';
-export class AllocationClusterRiskEmergingMarkets extends Rule {
+export class EconomicMarketClusterRiskEmergingMarkets extends Rule {
private currentValueInBaseCurrency: number;
private emergingMarketsValueInBaseCurrency: number;
@@ -13,7 +13,7 @@ export class AllocationClusterRiskEmergingMarkets extends Rule {
emergingMarketsValueInBaseCurrency: number
) {
super(exchangeRateDataService, {
- key: AllocationClusterRiskEmergingMarkets.name,
+ key: EconomicMarketClusterRiskEmergingMarkets.name,
name: 'Emerging Markets'
});
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
index ea83500c..d20c6691 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
@@ -22,9 +22,9 @@ import { takeUntil } from 'rxjs/operators';
})
export class FirePageComponent implements OnDestroy, OnInit {
public accountClusterRiskRules: PortfolioReportRule[];
- public allocationClusterRiskRules: PortfolioReportRule[];
public currencyClusterRiskRules: PortfolioReportRule[];
public deviceType: string;
+ public economicMarketClusterRiskRules: PortfolioReportRule[];
public emergencyFundRules: PortfolioReportRule[];
public feeRules: PortfolioReportRule[];
public fireWealth: Big;
@@ -204,15 +204,15 @@ export class FirePageComponent implements OnDestroy, OnInit {
}
) ?? null;
- this.allocationClusterRiskRules =
- portfolioReport.rules['allocationClusterRisk']?.filter(
+ this.currencyClusterRiskRules =
+ portfolioReport.rules['currencyClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
- this.currencyClusterRiskRules =
- portfolioReport.rules['currencyClusterRisk']?.filter(
+ this.economicMarketClusterRiskRules =
+ portfolioReport.rules['economicMarketClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
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 4eedca30..7a336b62 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.html
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html
@@ -176,7 +176,7 @@
- Allocation Cluster Risks
+ Economic Market Cluster Risks
@if (user?.subscription?.type === 'Basic') {
}
@@ -188,7 +188,7 @@
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
- [rules]="allocationClusterRiskRules"
+ [rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
diff --git a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts
index f38a8e6a..579d589b 100644
--- a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts
+++ b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts
@@ -1,10 +1,10 @@
export interface XRayRulesSettings {
AccountClusterRiskCurrentInvestment?: RuleSettings;
AccountClusterRiskSingleAccount?: RuleSettings;
- AllocationClusterRiskDevelopedMarkets?: RuleSettings;
- AllocationClusterRiskEmergingMarkets?: RuleSettings;
CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings;
CurrencyClusterRiskCurrentInvestment?: RuleSettings;
+ EconomicMarketClusterRiskDevelopedMarkets?: RuleSettings;
+ EconomicMarketClusterRiskEmergingMarkets?: RuleSettings;
EmergencyFundSetup?: RuleSettings;
FeeRatioInitialInvestment?: RuleSettings;
}
From ab44773945e2702477fb9034acc5ab7d6d623b31 Mon Sep 17 00:00:00 2001
From: dw-0
Date: Tue, 29 Oct 2024 17:29:03 +0100
Subject: [PATCH 03/65] Feature/Switch consistent-indexed-object-style eslint
rule from warn to off (#3999)
* Switch consistent-indexed-object-style eslint rule from warn to off
* Update changelog
---------
Signed-off-by: Dominik Willner
---
.eslintrc.json | 4 ++--
CHANGELOG.md | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.eslintrc.json b/.eslintrc.json
index 79d34dd0..75e36246 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -39,6 +39,7 @@
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": {
+ "@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
@@ -142,8 +143,7 @@
// The following rules are part of @typescript-eslint/stylistic-type-checked
// and can be remove once solved
- "@typescript-eslint/prefer-nullish-coalescing": "warn", // TODO: Requires strictNullChecks: true
- "@typescript-eslint/consistent-indexed-object-style": "warn"
+ "@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true
}
}
],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0dcbcc74..1a809350 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Renamed the static portfolio analysis rule from _Allocation Cluster Risk_ to _Economic Market Cluster Risk_ (Developed Markets and Emerging Markets)
- Improved the language localization for German (`de`)
- Switched the `consistent-generic-constructors` rule from `warn` to `error` in the `eslint` configuration
+- Switched the `consistent-indexed-object-style` rule from `warn` to `off` in the `eslint` configuration
- Switched the `consistent-type-assertions` rule from `warn` to `error` in the `eslint` configuration
- Switched the `prefer-optional-chain` rule from `warn` to `error` in the `eslint` configuration
From 53ae0d8aa7b1761a33a67f7a666301242ff5ef21 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 29 Oct 2024 18:47:54 +0100
Subject: [PATCH 04/65] Feature/upgrade nx to version 20.0.6 (#3996)
* Upgrade Nx to version 20.0.6
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 684 +++++++++-------------------------------------
package.json | 22 +-
3 files changed, 142 insertions(+), 565 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a809350..750e733d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Switched the `consistent-indexed-object-style` rule from `warn` to `off` in the `eslint` configuration
- Switched the `consistent-type-assertions` rule from `warn` to `error` in the `eslint` configuration
- Switched the `prefer-optional-chain` rule from `warn` to `error` in the `eslint` configuration
+- Upgraded `Nx` from version `20.0.3` to `20.0.6`
## 2.119.0 - 2024-10-26
diff --git a/package-lock.json b/package-lock.json
index 49260020..0761fdf9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.118.0",
+ "version": "2.119.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.118.0",
+ "version": "2.119.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -107,16 +107,16 @@
"@angular/pwa": "18.2.9",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
- "@nx/angular": "20.0.3",
- "@nx/cypress": "20.0.3",
- "@nx/eslint-plugin": "20.0.3",
- "@nx/jest": "20.0.3",
- "@nx/js": "20.0.3",
- "@nx/nest": "20.0.3",
- "@nx/node": "20.0.3",
- "@nx/storybook": "20.0.3",
- "@nx/web": "20.0.3",
- "@nx/workspace": "20.0.3",
+ "@nx/angular": "20.0.6",
+ "@nx/cypress": "20.0.6",
+ "@nx/eslint-plugin": "20.0.6",
+ "@nx/jest": "20.0.6",
+ "@nx/js": "20.0.6",
+ "@nx/nest": "20.0.6",
+ "@nx/node": "20.0.6",
+ "@nx/storybook": "20.0.6",
+ "@nx/web": "20.0.6",
+ "@nx/workspace": "20.0.6",
"@schematics/angular": "18.2.9",
"@simplewebauthn/types": "9.0.1",
"@storybook/addon-essentials": "8.3.6",
@@ -147,7 +147,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.1.0",
- "nx": "20.0.3",
+ "nx": "20.0.6",
"prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "5.21.1",
@@ -3688,63 +3688,6 @@
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
- "node_modules/@eslint/config-array": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
- "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@eslint/object-schema": "^2.1.4",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@eslint/config-array/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
- "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
@@ -3869,67 +3812,12 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
- "node_modules/@eslint/object-schema": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
- "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz",
- "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
"license": "MIT"
},
- "node_modules/@humanfs/core": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz",
- "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.5",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz",
- "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@humanfs/core": "^0.19.0",
- "@humanwhocodes/retry": "^0.3.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3992,22 +3880,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
- "node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
"node_modules/@inquirer/checkbox": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz",
@@ -6557,19 +6429,19 @@
}
},
"node_modules/@nx/angular": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-20.0.3.tgz",
- "integrity": "sha512-d9xekjP9onlRzW0Vz1My4USkiOuihZU3nM/SgGj7i2ZL7W/Fu81H7CAxlmsH93/XPresHaAnZSCSx0ofq5YyCA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-20.0.6.tgz",
+ "integrity": "sha512-0UfCEp4JeQEYMpUjaipHEH/V/GRHGCd+vgPN9EdhpkSqw2YyuBXlZiX1q0DgzMxZRRBRTB+p37FgRPu32lOI6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/enhanced": "0.6.6",
- "@nx/devkit": "20.0.3",
- "@nx/eslint": "20.0.3",
- "@nx/js": "20.0.3",
- "@nx/web": "20.0.3",
- "@nx/webpack": "20.0.3",
- "@nx/workspace": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/eslint": "20.0.6",
+ "@nx/js": "20.0.6",
+ "@nx/web": "20.0.6",
+ "@nx/webpack": "20.0.6",
+ "@nx/workspace": "20.0.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"@typescript-eslint/type-utils": "^8.0.0",
"chalk": "^4.1.0",
@@ -6662,15 +6534,15 @@
}
},
"node_modules/@nx/cypress": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-20.0.3.tgz",
- "integrity": "sha512-Bnm3Soa3aEIzmbJMtjmgGko5FVvBWh2I0stV+eMmLC8y9mCOQ3W4i+pXjpD6zVLAfZBF80W9e1ON7kdlfpM2Rw==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-20.0.6.tgz",
+ "integrity": "sha512-b26Ucgf+dAdTRlBGhFi8Xjeqw1mbUrxn3nwAOYNwuivc+CZCeokba5/orldNAlBlJKvHe0QmSAI3wpDjdU05Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/devkit": "20.0.3",
- "@nx/eslint": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/eslint": "20.0.6",
+ "@nx/js": "20.0.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"detect-port": "^1.5.1",
"tslib": "^2.3.0"
@@ -6685,9 +6557,9 @@
}
},
"node_modules/@nx/devkit": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.0.3.tgz",
- "integrity": "sha512-tB6iQ2opvipyy+4J0eImW/Nl8SoILPpDodwnThDJ2U2mflHG6/+3Wl6Q1hXieOnjT+ZE++ve91aYDEAi9OMwvA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-20.0.6.tgz",
+ "integrity": "sha512-vUjVVEJgfq/roCzDDZDXduwnhVXl1MM5No2UELUka2oNBK09pPigdFxzUNh8XvmOyFskCGDTLKH/dAO5yTD5Bg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6705,14 +6577,14 @@
}
},
"node_modules/@nx/eslint": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-20.0.3.tgz",
- "integrity": "sha512-uWS1jvGj5T2GOMRit8HqC0LOo1BxEzQejxEioIfLVaoO8bd67FdZQh2Tz3Qon9V05VXm8pEHQv/1NVNqanzgBQ==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-20.0.6.tgz",
+ "integrity": "sha512-07Ign5GQXZif6zHDR2oB4wkf2amSvoGhYWJ17fmqDsMF/nWYOohL+DbjAaqDORXWXL1bnmRBaj/lAkDNsmW3QA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/devkit": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/js": "20.0.6",
"semver": "^7.5.3",
"tslib": "^2.3.0",
"typescript": "~5.4.2"
@@ -6728,15 +6600,14 @@
}
},
"node_modules/@nx/eslint-plugin": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-20.0.3.tgz",
- "integrity": "sha512-KQi2rHwRQjQDqt7g4666LdKVBUNcHubX1MlXCB/f0ejCJunlybqK4aA+LiM0KIQpieevvIlAHJuTdZQ2M7q2HQ==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-20.0.6.tgz",
+ "integrity": "sha512-wFWg9X4dhRVY5pIAuqXLKTQSL3FzWHbV5kpg7S+y2X3jFg3pezqa8EDBkAcerSk7rour1G2hlXAfHX/W3HCrBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint/compat": "^1.1.1",
- "@nx/devkit": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/js": "20.0.6",
"@typescript-eslint/type-utils": "^8.0.0",
"@typescript-eslint/utils": "^8.0.0",
"chalk": "^4.1.0",
@@ -6756,105 +6627,6 @@
}
}
},
- "node_modules/@nx/eslint-plugin/node_modules/@eslint/compat": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.1.tgz",
- "integrity": "sha512-JbHG2TWuCeNzh87fXo+/46Z1LEo9DBA9T188d0fZgGxAD+cNyS6sx9fdiyxjGPBMyQVRlCutTByZ6a5+YMkF7g==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "peerDependencies": {
- "eslint": "^9.10.0"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/@eslint/eslintrc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
- "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/@eslint/js": {
- "version": "9.13.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
- "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/@nx/eslint-plugin/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
"node_modules/@nx/eslint-plugin/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -6871,19 +6643,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@nx/eslint-plugin/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/@nx/eslint-plugin/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -6901,150 +6660,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/@nx/eslint-plugin/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/eslint": {
- "version": "9.13.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz",
- "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.11.0",
- "@eslint/config-array": "^0.18.0",
- "@eslint/core": "^0.7.0",
- "@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.13.0",
- "@eslint/plugin-kit": "^0.2.0",
- "@humanfs/node": "^0.16.5",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.3.1",
- "@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.1.0",
- "eslint-visitor-keys": "^4.1.0",
- "espree": "^10.2.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3",
- "text-table": "^0.2.0"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/eslint-visitor-keys": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
- "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/espree": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
- "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "acorn": "^8.12.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.1.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
"node_modules/@nx/eslint-plugin/node_modules/globals": {
"version": "15.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz",
@@ -7068,45 +6683,6 @@
"node": ">=8"
}
},
- "node_modules/@nx/eslint-plugin/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/@nx/eslint-plugin/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/@nx/eslint-plugin/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@nx/eslint-plugin/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -7135,16 +6711,16 @@
}
},
"node_modules/@nx/jest": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-20.0.3.tgz",
- "integrity": "sha512-ZC9OPSh1htpYEh+kGZAew5r1pLtOCZo3odqW7/DalCti2XOTVit8yuw1DahIqrzZ3BzcTq+q9W9Ng17mMVCaCA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-20.0.6.tgz",
+ "integrity": "sha512-/v9NavOOWcUpzgbjfYip0zipneJPhKUQd5rU3bTr0CqCJw0I+YQXotToUkzzMQYT6zmNrq7ySTMH1N8rXdy7NQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/reporters": "^29.4.1",
"@jest/test-result": "^29.4.1",
- "@nx/devkit": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/js": "20.0.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"chalk": "^4.1.0",
"identity-obj-proxy": "3.0.0",
@@ -7215,9 +6791,9 @@
}
},
"node_modules/@nx/js": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/js/-/js-20.0.3.tgz",
- "integrity": "sha512-UbltxJyfEXL586kk7yxOTNHtigd7rq7atmcOmMphcxbeWk9HzeowVh6j6OA4MAKwYauomjCqsJbvWURI8qf+pg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/js/-/js-20.0.6.tgz",
+ "integrity": "sha512-/bAMtcgKX1Te3yCzbbv+QQLnFwb6SxE0iCc6EzxiLepmGhnd0iOArUqepB1mVipfeaO37n00suFjFv1xsaqLHg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7228,8 +6804,8 @@
"@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6",
- "@nx/devkit": "20.0.3",
- "@nx/workspace": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/workspace": "20.0.6",
"@zkochan/js-yaml": "0.0.7",
"babel-plugin-const-enum": "^1.0.1",
"babel-plugin-macros": "^2.8.0",
@@ -7503,17 +7079,17 @@
}
},
"node_modules/@nx/nest": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-20.0.3.tgz",
- "integrity": "sha512-8Il3BcyiJfj5vAXszlX+QqRlvAU9eudoc59q8GIXzU9vyrjrYcazpxkQr+qJ2q8mLdvJYJYMpA4ilbE6y6PQPw==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-20.0.6.tgz",
+ "integrity": "sha512-vACNZ+jTURJfVIpLQURgbR12O7mOqoVjCSfqbXBIC9pc4kYqShPW0SnAybwxKR+zpWGeO1nel7VXEH95HgAXuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nestjs/schematics": "^9.1.0",
- "@nx/devkit": "20.0.3",
- "@nx/eslint": "20.0.3",
- "@nx/js": "20.0.3",
- "@nx/node": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/eslint": "20.0.6",
+ "@nx/js": "20.0.6",
+ "@nx/node": "20.0.6",
"tslib": "^2.3.0"
}
},
@@ -7645,23 +7221,23 @@
}
},
"node_modules/@nx/node": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/node/-/node-20.0.3.tgz",
- "integrity": "sha512-50JqKEVRmh2g9bxBxB0hDVzNae6rf9d5Iu8bTxpF55h6kivdoiYF793/awpxCpE6XPCij9IafeoaT77Ug8dQYA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/node/-/node-20.0.6.tgz",
+ "integrity": "sha512-/6khofVKgpdglkSE6XDz9tk4kCeEXQaIPOH1PgWqY25hoim/VSXjZ1XMVmPvnvd7m2lsFLDrqZlwIGWTrT2cFw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/devkit": "20.0.3",
- "@nx/eslint": "20.0.3",
- "@nx/jest": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/eslint": "20.0.6",
+ "@nx/jest": "20.0.6",
+ "@nx/js": "20.0.6",
"tslib": "^2.3.0"
}
},
"node_modules/@nx/nx-darwin-arm64": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.0.3.tgz",
- "integrity": "sha512-/wjxSuQZOHwDopNAfuh2BTsaDtDECjTDrKHJdTknrSVjdsB2b1hwSdL7Ct0PXBiSnf+0gfYBR2fuPmLZYb3AXA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.0.6.tgz",
+ "integrity": "sha512-SUVfEqzl/iy2NzTbpY2E9lHSxs8c9QERhTILp5OOt0Vgmhn9iTxVEIoSCjzz/MyX066eARarUymUyK4JCg3mqw==",
"cpu": [
"arm64"
],
@@ -7676,9 +7252,9 @@
}
},
"node_modules/@nx/nx-darwin-x64": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.0.3.tgz",
- "integrity": "sha512-Gobgkvsx61P5TI0uuDQTI/D2AXJt3xnBuAWQ4V/NW/OpkvL8j/q8zk81uK0tumVvIc4p5kSlGmQ46/ytSrdqvg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.0.6.tgz",
+ "integrity": "sha512-JI0kcJGBeIj3sb+kC0nZMOSXFnvCOtGbAVK3HHJ9DSRxckLq5bImwqdfYSNJL9ocU8YU+Qds/SercEV02gQOkQ==",
"cpu": [
"x64"
],
@@ -7693,9 +7269,9 @@
}
},
"node_modules/@nx/nx-freebsd-x64": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.0.3.tgz",
- "integrity": "sha512-nbYp89BP0z0DzuaUH/yVVhCbL96vUUaKmCVmmdlvQRgiaX89BChAMuEdLNSeaDHFrhgTYB87ku3Ok6DRCAIOcg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.0.6.tgz",
+ "integrity": "sha512-om9Sh5Pg5aRDlBWyHMAX/1swLSj2pCqk1grXN6RcJ8O3tXLI35fj4wz6sPDRASwC1xuHwET2DG/20Ec6n1Ko3A==",
"cpu": [
"x64"
],
@@ -7710,9 +7286,9 @@
}
},
"node_modules/@nx/nx-linux-arm-gnueabihf": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.0.3.tgz",
- "integrity": "sha512-eKIYJPvXO/N1FjteZHC4DLV0u+2h70RmrDQODPztfl3mI5AjCwFdLf9RPN1D+SuNdfK1WwZIszY+FiVxrpK19A==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.0.6.tgz",
+ "integrity": "sha512-XIomXUqnH3w1aqRu0T+Wcn9roXT1bG1PjuX+bmGLkSiZ+ZyY/zYfhg6WKbox3TqQcdC1jNUkzEQlLGcfWaGc6w==",
"cpu": [
"arm"
],
@@ -7727,9 +7303,9 @@
}
},
"node_modules/@nx/nx-linux-arm64-gnu": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.0.3.tgz",
- "integrity": "sha512-CDFy2WNsMZvxshtGdFV/yCux1XkLtcqh0FiitNvGdgNugXXp3CLVEUx6dI3VBuIBNGbfozdr7n+fuXN6F2S4MQ==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.0.6.tgz",
+ "integrity": "sha512-Asx2F+WtauELssmrQf1y4ZeiMIsgbL/+PnD+WgbvHVWbl7cRUfLJqEhOR5fQG6CiNTIXvOyzXMoaJVA9hTub+Q==",
"cpu": [
"arm64"
],
@@ -7744,9 +7320,9 @@
}
},
"node_modules/@nx/nx-linux-arm64-musl": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.0.3.tgz",
- "integrity": "sha512-BGrSRNPuDyj0yeP2MyzF1MMij1KO4Q/2YSgBbYzVSc8JdrUqf+3rqI8VXNTr3FcAKMTPgFjkFZ3XD3s/62gsdg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.0.6.tgz",
+ "integrity": "sha512-4lyBaLWSv7VNMOXWxtuDNiSOE4M5QGiVHimSvQ9PBwgnrvEuc6fCv/Nc8ecU0rINHRQJruYMTD/kKBCsahwJUQ==",
"cpu": [
"arm64"
],
@@ -7761,9 +7337,9 @@
}
},
"node_modules/@nx/nx-linux-x64-gnu": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.0.3.tgz",
- "integrity": "sha512-xGGjQ8q5XuF0/432APvAi/OSMdR3LZ1yQ9hYh+JGvM5wh44I3UbgBXRCJlsHp+t2hdlilF6kpaeMSiP1Z9CEbg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.0.6.tgz",
+ "integrity": "sha512-HGZzX7un/rJvADKwN27HM0e3Gx19hSndCoqZUtqHgrFRdUvTfHTWNpT6uZ5XW/5bNnRKdUinY9DHhlYpE0u4KQ==",
"cpu": [
"x64"
],
@@ -7778,9 +7354,9 @@
}
},
"node_modules/@nx/nx-linux-x64-musl": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.0.3.tgz",
- "integrity": "sha512-fTmZNbq3QQF5BLGPB8PGuFuNo3s2F86IQDOUYWpjXiuKjoI1Y5yM14RQpHLwYOGnUNoKYOhlv/JAyFrDX6ALZA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.0.6.tgz",
+ "integrity": "sha512-OwMq+ozzCOCtAViOouHbe/MXqep/q4EKg44YelUqVNIe/2XimcIfMlBQFk1DOcmibesxa3yWMKAdg2IGUnG+pQ==",
"cpu": [
"x64"
],
@@ -7795,9 +7371,9 @@
}
},
"node_modules/@nx/nx-win32-arm64-msvc": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.0.3.tgz",
- "integrity": "sha512-hdtfg9pIzhtLqqGvsTemQYwe+kqqL1JGNgrlf3V59HSbbAADYZbHnliujoRybJo7dpeS/DDTNMNeblg99tFQLA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.0.6.tgz",
+ "integrity": "sha512-2D8TIjyi5dJLy4cx8u7YKunW6+EG9FAuBUo75qMCozTBw1EPTK2lzwLE2d8C7WOxBA148O2wzD5uiX1vCt2Tzg==",
"cpu": [
"arm64"
],
@@ -7812,9 +7388,9 @@
}
},
"node_modules/@nx/nx-win32-x64-msvc": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.0.3.tgz",
- "integrity": "sha512-HcqE8AlWuwcsIOj0OnKDQ3q7L0RZsOrBRhDRKbJeUnIFz/t2R3q8Y6trrqTyFAafgW6JNLBp+tgcUyfHPUy/eQ==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.0.6.tgz",
+ "integrity": "sha512-B83kpN1+KdJ97P0Rw/KRyZ5fZPtKimvwg/TAJdWR1D8oqdrpaZwgTd9dcsTNavvynUsPqM3GdjmFKzTYTZ4MFQ==",
"cpu": [
"x64"
],
@@ -7829,30 +7405,30 @@
}
},
"node_modules/@nx/storybook": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-20.0.3.tgz",
- "integrity": "sha512-Nt5iWUYDEvA+S8I9aFnF5fhODV+H74XR+lS8kza+o0K4kRqhdvzp7WB3SanEuY/k3kbhP7M99dbBRZXJi3Sgdg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-20.0.6.tgz",
+ "integrity": "sha512-eqQKs67bRb9vutCt+dcR5CUhnSiQ2X82cYNryHEu/u8qE0LRfmCxxWh1DUNGxz1v1SYquo6RBo0qORm8oef3Pg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/cypress": "20.0.3",
- "@nx/devkit": "20.0.3",
- "@nx/eslint": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/cypress": "20.0.6",
+ "@nx/devkit": "20.0.6",
+ "@nx/eslint": "20.0.6",
+ "@nx/js": "20.0.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"semver": "^7.5.3",
"tslib": "^2.3.0"
}
},
"node_modules/@nx/web": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/web/-/web-20.0.3.tgz",
- "integrity": "sha512-b3KpUeA0cI9JIpRBYEk/4sIs9nCI6RcXCmxFoyW60vYsr2VtPZjtLKbo3bBT7HLOk3iwAYYWzCY1cu0Xzig3Lg==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/web/-/web-20.0.6.tgz",
+ "integrity": "sha512-lYu9FddREZYbjbjS9YYnXu+uGQUB6MptNvPNSvYRRUcdq7c8Kh10P21YyK2Ox7FsEUeqly+XVvhlKNXeQF5anw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/devkit": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/js": "20.0.6",
"detect-port": "^1.5.1",
"http-server": "^14.1.0",
"picocolors": "^1.1.0",
@@ -7860,17 +7436,17 @@
}
},
"node_modules/@nx/webpack": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-20.0.3.tgz",
- "integrity": "sha512-r9oBx1BV3zm6292TZnQd+6dwSx9Gixl5AgneJmAVQvT65moLfg2bL8t0G6sSodxYChcXVB7mJriXDAJjMbb48w==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-20.0.6.tgz",
+ "integrity": "sha512-LvjkJ0yVXDCNgxxIKYLMtEJVVdvBVHcB9mgwPdBfl38STAf/HwTuB7XXTZVYu+m9iPusU1VpFpaUlbpQN79f8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.2",
"@module-federation/enhanced": "^0.6.0",
"@module-federation/sdk": "^0.6.0",
- "@nx/devkit": "20.0.3",
- "@nx/js": "20.0.3",
+ "@nx/devkit": "20.0.6",
+ "@nx/js": "20.0.6",
"@phenomnomnominal/tsquery": "~5.0.1",
"ajv": "^8.12.0",
"autoprefixer": "^10.4.9",
@@ -8526,16 +8102,16 @@
}
},
"node_modules/@nx/workspace": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-20.0.3.tgz",
- "integrity": "sha512-ctStDr9UlXt63v9wC1qS9lqLABSDfcfCH/FtQ6ZF5RjWIkzZS672g29gkT83L9B87dfRJYCH8yGGbvMJzq0qRA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-20.0.6.tgz",
+ "integrity": "sha512-A7lle47I4JggbhXoUVvkuvULqF0Xejy4LpE0txz9OIM5a9HxW1aIHYYQFuROBuVlMFxAJusPeYFJCCvb+qBxKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@nx/devkit": "20.0.3",
+ "@nx/devkit": "20.0.6",
"chalk": "^4.1.0",
"enquirer": "~2.3.6",
- "nx": "20.0.3",
+ "nx": "20.0.6",
"tslib": "^2.3.0",
"yargs-parser": "21.1.1"
}
@@ -27766,9 +27342,9 @@
"license": "MIT"
},
"node_modules/nx": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/nx/-/nx-20.0.3.tgz",
- "integrity": "sha512-6ZuZ09IdMIwbklKqEwUAHspuVMsDr7TIcCyeytmdDC1XbA+Tbb93wriyJyiI9EBQw4StrlJF9vSXAZsuDiOKeA==",
+ "version": "20.0.6",
+ "resolved": "https://registry.npmjs.org/nx/-/nx-20.0.6.tgz",
+ "integrity": "sha512-z8PMPEXxtADwxsNXamZdDbx65fcNcR4gTmX7N94GKmpZNrjwd3m7RcnoYgQp5vA8kFQkMR+320mtq5NkGJPZvg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -27811,16 +27387,16 @@
"nx-cloud": "bin/nx-cloud.js"
},
"optionalDependencies": {
- "@nx/nx-darwin-arm64": "20.0.3",
- "@nx/nx-darwin-x64": "20.0.3",
- "@nx/nx-freebsd-x64": "20.0.3",
- "@nx/nx-linux-arm-gnueabihf": "20.0.3",
- "@nx/nx-linux-arm64-gnu": "20.0.3",
- "@nx/nx-linux-arm64-musl": "20.0.3",
- "@nx/nx-linux-x64-gnu": "20.0.3",
- "@nx/nx-linux-x64-musl": "20.0.3",
- "@nx/nx-win32-arm64-msvc": "20.0.3",
- "@nx/nx-win32-x64-msvc": "20.0.3"
+ "@nx/nx-darwin-arm64": "20.0.6",
+ "@nx/nx-darwin-x64": "20.0.6",
+ "@nx/nx-freebsd-x64": "20.0.6",
+ "@nx/nx-linux-arm-gnueabihf": "20.0.6",
+ "@nx/nx-linux-arm64-gnu": "20.0.6",
+ "@nx/nx-linux-arm64-musl": "20.0.6",
+ "@nx/nx-linux-x64-gnu": "20.0.6",
+ "@nx/nx-linux-x64-musl": "20.0.6",
+ "@nx/nx-win32-arm64-msvc": "20.0.6",
+ "@nx/nx-win32-x64-msvc": "20.0.6"
},
"peerDependencies": {
"@swc-node/register": "^1.8.0",
diff --git a/package.json b/package.json
index b83ce5c5..75826156 100644
--- a/package.json
+++ b/package.json
@@ -153,16 +153,16 @@
"@angular/pwa": "18.2.9",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
- "@nx/angular": "20.0.3",
- "@nx/cypress": "20.0.3",
- "@nx/eslint-plugin": "20.0.3",
- "@nx/jest": "20.0.3",
- "@nx/js": "20.0.3",
- "@nx/nest": "20.0.3",
- "@nx/node": "20.0.3",
- "@nx/storybook": "20.0.3",
- "@nx/web": "20.0.3",
- "@nx/workspace": "20.0.3",
+ "@nx/angular": "20.0.6",
+ "@nx/cypress": "20.0.6",
+ "@nx/eslint-plugin": "20.0.6",
+ "@nx/jest": "20.0.6",
+ "@nx/js": "20.0.6",
+ "@nx/nest": "20.0.6",
+ "@nx/node": "20.0.6",
+ "@nx/storybook": "20.0.6",
+ "@nx/web": "20.0.6",
+ "@nx/workspace": "20.0.6",
"@schematics/angular": "18.2.9",
"@simplewebauthn/types": "9.0.1",
"@storybook/addon-essentials": "8.3.6",
@@ -193,7 +193,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.1.0",
- "nx": "20.0.3",
+ "nx": "20.0.6",
"prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "5.21.1",
From 10e725b51adc1104ec8eefc8c6308c86f55a639c Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 30 Oct 2024 21:39:20 +0100
Subject: [PATCH 05/65] Feature/Add dataProviderGhostfolioDailyRequests to
Analytics (#4001)
* Add dataProviderGhostfolioDailyRequests to Analytics
---
apps/api/src/app/user/user.service.ts | 53 ++++++++++++-------
apps/api/src/services/cron.service.ts | 13 ++++-
.../migration.sql | 2 +
prisma/schema.prisma | 11 ++--
4 files changed, 54 insertions(+), 25 deletions(-)
create mode 100644 prisma/migrations/20241029190323_added_data_provider_ghostfolio_daily_requests_to_analytics/migration.sql
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 288e2aba..443a2a05 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -37,7 +37,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, User } from '@prisma/client';
-import { differenceInDays } from 'date-fns';
+import { differenceInDays, subDays } from 'date-fns';
import { sortBy, without } from 'lodash';
const crypto = require('crypto');
@@ -60,6 +60,13 @@ export class UserService {
return this.prismaService.user.count(args);
}
+ public createAccessToken(password: string, salt: string): string {
+ const hash = crypto.createHmac('sha512', salt);
+ hash.update(password);
+
+ return hash.digest('hex');
+ }
+
public async getUser(
{ Account, id, permissions, Settings, subscription }: UserWithSettings,
aLocale = locale
@@ -358,13 +365,6 @@ export class UserService {
});
}
- public createAccessToken(password: string, salt: string): string {
- const hash = crypto.createHmac('sha512', salt);
- hash.update(password);
-
- return hash.digest('hex');
- }
-
public async createUser({
data
}: {
@@ -426,17 +426,6 @@ export class UserService {
return user;
}
- public async updateUser(params: {
- where: Prisma.UserWhereUniqueInput;
- data: Prisma.UserUpdateInput;
- }): Promise {
- const { where, data } = params;
- return this.prismaService.user.update({
- data,
- where
- });
- }
-
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise {
try {
await this.prismaService.access.deleteMany({
@@ -473,6 +462,32 @@ export class UserService {
});
}
+ public async resetAnalytics() {
+ return this.prismaService.analytics.updateMany({
+ data: {
+ dataProviderGhostfolioDailyRequests: 0
+ },
+ where: {
+ updatedAt: {
+ gte: subDays(new Date(), 1)
+ }
+ }
+ });
+ }
+
+ public async updateUser({
+ data,
+ where
+ }: {
+ data: Prisma.UserUpdateInput;
+ where: Prisma.UserWhereUniqueInput;
+ }): Promise {
+ return this.prismaService.user.update({
+ data,
+ where
+ });
+ }
+
public async updateUserSetting({
emitPortfolioChangedEvent,
userId,
diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts
index 17e970c1..7a1b30b5 100644
--- a/apps/api/src/services/cron.service.ts
+++ b/apps/api/src/services/cron.service.ts
@@ -1,3 +1,4 @@
+import { UserService } from '@ghostfolio/api/app/user/user.service';
import {
DATA_GATHERING_QUEUE_PRIORITY_LOW,
GATHER_ASSET_PROFILE_PROCESS,
@@ -9,6 +10,7 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
+import { ConfigurationService } from './configuration/configuration.service';
import { ExchangeRateDataService } from './exchange-rate-data/exchange-rate-data.service';
import { PropertyService } from './property/property.service';
import { DataGatheringService } from './queues/data-gathering/data-gathering.service';
@@ -19,10 +21,12 @@ export class CronService {
private static readonly EVERY_SUNDAY_AT_LUNCH_TIME = '0 12 * * 0';
public constructor(
+ private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly propertyService: PropertyService,
- private readonly twitterBotService: TwitterBotService
+ private readonly twitterBotService: TwitterBotService,
+ private readonly userService: UserService
) {}
@Cron(CronExpression.EVERY_HOUR)
@@ -42,6 +46,13 @@ export class CronService {
this.twitterBotService.tweetFearAndGreedIndex();
}
+ @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
+ public async runEveryDayAtMidnight() {
+ if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
+ this.userService.resetAnalytics();
+ }
+ }
+
@Cron(CronService.EVERY_SUNDAY_AT_LUNCH_TIME)
public async runEverySundayAtTwelvePm() {
if (await this.isDataGatheringEnabled()) {
diff --git a/prisma/migrations/20241029190323_added_data_provider_ghostfolio_daily_requests_to_analytics/migration.sql b/prisma/migrations/20241029190323_added_data_provider_ghostfolio_daily_requests_to_analytics/migration.sql
new file mode 100644
index 00000000..a9566dd8
--- /dev/null
+++ b/prisma/migrations/20241029190323_added_data_provider_ghostfolio_daily_requests_to_analytics/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Analytics" ADD COLUMN "dataProviderGhostfolioDailyRequests" INTEGER NOT NULL DEFAULT 0;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 9fa55076..7f386a71 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -65,11 +65,12 @@ model AccountBalance {
}
model Analytics {
- activityCount Int @default(0)
- country String?
- updatedAt DateTime @updatedAt
- userId String @id
- User User @relation(fields: [userId], onDelete: Cascade, references: [id])
+ activityCount Int @default(0)
+ country String?
+ dataProviderGhostfolioDailyRequests Int @default(0)
+ updatedAt DateTime @updatedAt
+ userId String @id
+ User User @relation(fields: [userId], onDelete: Cascade, references: [id])
@@index([updatedAt])
}
From db53ef54c4ba0c7cf25ceb4ce145bc791339bb6d Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 30 Oct 2024 21:39:51 +0100
Subject: [PATCH 06/65] Feature/use log levels to conditionally log prisma
query events (#4003)
* Use LOG_LEVELS for prisma query events
* Update changelog
---
CHANGELOG.md | 4 ++++
apps/api/src/services/prisma/prisma.module.ts | 5 ++--
.../api/src/services/prisma/prisma.service.ts | 24 ++++++++++++++++++-
3 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 750e733d..44db5ad5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+- Added support for log levels (`LOG_LEVELS`) to conditionally log `prisma` query events (`debug` or `verbose`)
+
### Changed
- Restructured the resources page
diff --git a/apps/api/src/services/prisma/prisma.module.ts b/apps/api/src/services/prisma/prisma.module.ts
index 3875c8ca..24da6104 100644
--- a/apps/api/src/services/prisma/prisma.module.ts
+++ b/apps/api/src/services/prisma/prisma.module.ts
@@ -1,9 +1,10 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Module } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
@Module({
- providers: [PrismaService],
- exports: [PrismaService]
+ exports: [PrismaService],
+ providers: [ConfigService, PrismaService]
})
export class PrismaModule {}
diff --git a/apps/api/src/services/prisma/prisma.service.ts b/apps/api/src/services/prisma/prisma.service.ts
index e99d6ecf..4673cbd1 100644
--- a/apps/api/src/services/prisma/prisma.service.ts
+++ b/apps/api/src/services/prisma/prisma.service.ts
@@ -1,16 +1,38 @@
import {
Injectable,
Logger,
+ LogLevel,
OnModuleDestroy,
OnModuleInit
} from '@nestjs/common';
-import { PrismaClient } from '@prisma/client';
+import { ConfigService } from '@nestjs/config';
+import { Prisma, PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
+ public constructor(configService: ConfigService) {
+ let customLogLevels: LogLevel[];
+
+ try {
+ customLogLevels = JSON.parse(
+ configService.get('LOG_LEVELS')
+ ) as LogLevel[];
+ } catch {}
+
+ const log: Prisma.LogDefinition[] =
+ customLogLevels?.includes('debug') || customLogLevels?.includes('verbose')
+ ? [{ emit: 'stdout', level: 'query' }]
+ : [];
+
+ super({
+ log,
+ errorFormat: 'colorless'
+ });
+ }
+
public async onModuleInit() {
try {
await this.$connect();
From d7f69020c21be3f611e89ed9dd0755b07929a7b3 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 30 Oct 2024 21:41:47 +0100
Subject: [PATCH 07/65] Release 2.120.0 (#4004)
---
CHANGELOG.md | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44db5ad5..6feab65b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## 2.120.0 - 2024-10-30
### Added
diff --git a/package.json b/package.json
index 75826156..134d72f9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.119.0",
+ "version": "2.120.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From 1ee9cd3de14e68c8ac459a8d1e6b3376ba93dfbb Mon Sep 17 00:00:00 2001
From: dw-0
Date: Thu, 31 Oct 2024 13:37:56 +0100
Subject: [PATCH 08/65] Feature/set stack and container names in docker-compose
files (#4000)
* Set stack and container names in docker-compose files
* Update changelog
---------
Signed-off-by: Dominik Willner
---
CHANGELOG.md | 6 ++++++
docker/docker-compose.build.yml | 4 ++++
docker/docker-compose.dev.yml | 5 +++--
docker/docker-compose.yml | 4 ++++
4 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6feab65b..0ed7f9af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Added
+
+- Set the stack and container names in the `docker-compose` files (`docker-compose.yml`, `docker-compose.build.yml` and `docker-compose.dev.yml`)
+
## 2.120.0 - 2024-10-30
### Added
diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml
index 02442c0c..9cc8eb1d 100644
--- a/docker/docker-compose.build.yml
+++ b/docker/docker-compose.build.yml
@@ -1,6 +1,8 @@
+name: ghostfolio_build
services:
ghostfolio:
build: ../
+ container_name: gf-application-build
init: true
env_file:
- ../.env
@@ -23,6 +25,7 @@ services:
postgres:
image: docker.io/library/postgres:15
+ container_name: gf-postgres-build
env_file:
- ../.env
healthcheck:
@@ -35,6 +38,7 @@ services:
redis:
image: docker.io/library/redis:alpine
+ container_name: gf-redis-build
env_file:
- ../.env
command: ['redis-server', '--requirepass', $REDIS_PASSWORD]
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 91f8f2c0..39a1d56e 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -1,7 +1,8 @@
+name: ghostfolio_dev
services:
postgres:
image: docker.io/library/postgres:15
- container_name: postgres
+ container_name: gf-postgres-dev
restart: unless-stopped
env_file:
- ../.env
@@ -12,7 +13,7 @@ services:
redis:
image: docker.io/library/redis:alpine
- container_name: redis
+ container_name: gf-redis-dev
restart: unless-stopped
env_file:
- ../.env
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 642912b7..b8215c97 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,6 +1,8 @@
+name: ghostfolio
services:
ghostfolio:
image: docker.io/ghostfolio/ghostfolio:latest
+ container_name: gf-application
init: true
cap_drop:
- ALL
@@ -27,6 +29,7 @@ services:
postgres:
image: docker.io/library/postgres:15
+ container_name: gf-postgres
cap_drop:
- ALL
cap_add:
@@ -49,6 +52,7 @@ services:
redis:
image: docker.io/library/redis:alpine
+ container_name: gf-redis
user: '999:1000'
cap_drop:
- ALL
From 45c0487ba75702ce9e140f6b59568e11b5b4cc5f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 31 Oct 2024 17:34:18 +0100
Subject: [PATCH 09/65] Bugfix/Set stack and container names in docker-compose
files (#4005)
* Set stack and container names in docker-compose files
---
docker/docker-compose.build.yml | 2 +-
docker/docker-compose.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml
index 9cc8eb1d..96829ad3 100644
--- a/docker/docker-compose.build.yml
+++ b/docker/docker-compose.build.yml
@@ -2,7 +2,7 @@ name: ghostfolio_build
services:
ghostfolio:
build: ../
- container_name: gf-application-build
+ container_name: ghostfolio-build
init: true
env_file:
- ../.env
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b8215c97..c6ec5b3d 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -2,7 +2,7 @@ name: ghostfolio
services:
ghostfolio:
image: docker.io/ghostfolio/ghostfolio:latest
- container_name: gf-application
+ container_name: ghostfolio
init: true
cap_drop:
- ALL
From 0004c5dabead28ac545e81ed731a4df97fd549df Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:36:33 +0100
Subject: [PATCH 10/65] Feature/revert permissions on entrypoint.sh in
dockerfile (#4007)
* Revert permissions
* Update changelog
---
CHANGELOG.md | 4 ++++
Dockerfile | 1 -
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ed7f9af..397cc998 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Set the stack and container names in the `docker-compose` files (`docker-compose.yml`, `docker-compose.build.yml` and `docker-compose.dev.yml`)
+### Changed
+
+- Reverted the permissions (`chmod 0700`) on `entrypoint.sh` in the `Dockerfile`
+
## 2.120.0 - 2024-10-30
### Added
diff --git a/Dockerfile b/Dockerfile
index 0e5c0d27..e6c38f27 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,7 +61,6 @@ RUN apt-get update && apt-get install -y --no-install-suggests \
COPY --chown=node:node --from=builder /ghostfolio/dist/apps /ghostfolio/apps
COPY --chown=node:node ./docker/entrypoint.sh /ghostfolio/entrypoint.sh
-RUN chmod 0700 /ghostfolio/entrypoint.sh
WORKDIR /ghostfolio/apps/api
EXPOSE ${PORT:-3333}
USER node
From 5f56812125e354cc187e953d74f1c4a884a8127a Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:44:57 +0100
Subject: [PATCH 11/65] Feature/extend promotion system (#4008)
* Extend promotion system
---
apps/api/src/app/info/info.service.ts | 14 ++---
.../app/subscription/subscription.service.ts | 57 +++++++++++++++++--
apps/client/src/app/app.component.html | 1 +
apps/client/src/app/app.component.ts | 13 +++++
.../components/header/header.component.html | 27 +++++++--
.../app/components/header/header.component.ts | 1 +
.../user-account-membership.component.ts | 19 +++++--
.../user-account-membership.html | 10 ++++
.../pages/pricing/pricing-page.component.ts | 24 +++++---
.../src/app/pages/pricing/pricing-page.html | 18 ++++++
.../product-page.component.ts | 4 +-
libs/common/src/lib/interfaces/index.ts | 4 +-
.../src/lib/interfaces/info-item.interface.ts | 6 +-
.../subscription-offer.interface.ts | 9 +++
.../lib/interfaces/subscription.interface.ts | 6 --
.../src/lib/interfaces/user.interface.ts | 4 +-
libs/common/src/lib/types/index.ts | 4 +-
...type.ts => subscription-offer-key.type.ts} | 2 +-
.../src/lib/types/user-with-settings.type.ts | 4 +-
19 files changed, 177 insertions(+), 50 deletions(-)
create mode 100644 libs/common/src/lib/interfaces/subscription-offer.interface.ts
delete mode 100644 libs/common/src/lib/interfaces/subscription.interface.ts
rename libs/common/src/lib/types/{subscription-offer.type.ts => subscription-offer-key.type.ts} (71%)
diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts
index bd291c51..f81ddd71 100644
--- a/apps/api/src/app/info/info.service.ts
+++ b/apps/api/src/app/info/info.service.ts
@@ -23,10 +23,10 @@ import {
import {
InfoItem,
Statistics,
- Subscription
+ SubscriptionOffer
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
-import { SubscriptionOffer } from '@ghostfolio/common/types';
+import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@@ -101,7 +101,7 @@ export class InfoService {
isUserSignupEnabled,
platforms,
statistics,
- subscriptions
+ subscriptionOffers
] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(),
@@ -110,7 +110,7 @@ export class InfoService {
orderBy: { name: 'asc' }
}),
this.getStatistics(),
- this.getSubscriptions()
+ this.getSubscriptionOffers()
]);
if (isUserSignupEnabled) {
@@ -125,7 +125,7 @@ export class InfoService {
isReadOnlyMode,
platforms,
statistics,
- subscriptions,
+ subscriptionOffers,
baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies()
};
@@ -314,8 +314,8 @@ export class InfoService {
return statistics;
}
- private async getSubscriptions(): Promise<{
- [offer in SubscriptionOffer]: Subscription;
+ private async getSubscriptionOffers(): Promise<{
+ [offer in SubscriptionOfferKey]: SubscriptionOffer;
}> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined;
diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts
index 7c1df023..47e6db00 100644
--- a/apps/api/src/app/subscription/subscription.service.ts
+++ b/apps/api/src/app/subscription/subscription.service.ts
@@ -1,8 +1,16 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
-import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
+import { PropertyService } from '@ghostfolio/api/services/property/property.service';
+import {
+ DEFAULT_LANGUAGE_CODE,
+ PROPERTY_STRIPE_CONFIG
+} from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
-import { SubscriptionOffer, UserWithSettings } from '@ghostfolio/common/types';
+import { SubscriptionOffer } from '@ghostfolio/common/interfaces';
+import {
+ SubscriptionOfferKey,
+ UserWithSettings
+} from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Injectable, Logger } from '@nestjs/common';
@@ -17,7 +25,8 @@ export class SubscriptionService {
public constructor(
private readonly configurationService: ConfigurationService,
- private readonly prismaService: PrismaService
+ private readonly prismaService: PrismaService,
+ private readonly propertyService: PropertyService
) {
this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'),
@@ -36,6 +45,18 @@ export class SubscriptionService {
priceId: string;
user: UserWithSettings;
}) {
+ const subscriptionOffers: {
+ [offer in SubscriptionOfferKey]: SubscriptionOffer;
+ } =
+ ((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
+ {};
+
+ const subscriptionOffer = Object.values(subscriptionOffers).find(
+ (subscriptionOffer) => {
+ return subscriptionOffer.priceId === priceId;
+ }
+ );
+
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
cancel_url: `${this.configurationService.get('ROOT_URL')}/${
user.Settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE
@@ -47,6 +68,13 @@ export class SubscriptionService {
quantity: 1
}
],
+ locale:
+ (user.Settings?.settings
+ ?.language as Stripe.Checkout.SessionCreateParams.Locale) ??
+ DEFAULT_LANGUAGE_CODE,
+ metadata: subscriptionOffer
+ ? { subscriptionOffer: JSON.stringify(subscriptionOffer) }
+ : {},
mode: 'payment',
payment_method_types: ['card'],
success_url: `${this.configurationService.get(
@@ -73,17 +101,25 @@ export class SubscriptionService {
public async createSubscription({
duration = '1 year',
+ durationExtension,
price,
userId
}: {
duration?: StringValue;
+ durationExtension?: StringValue;
price: number;
userId: string;
}) {
+ let expiresAt = addMilliseconds(new Date(), ms(duration));
+
+ if (durationExtension) {
+ expiresAt = addMilliseconds(expiresAt, ms(durationExtension));
+ }
+
await this.prismaService.subscription.create({
data: {
+ expiresAt,
price,
- expiresAt: addMilliseconds(new Date(), ms(duration)),
User: {
connect: {
id: userId
@@ -95,10 +131,21 @@ export class SubscriptionService {
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try {
+ let durationExtension: StringValue;
+
const session =
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
+ const subscriptionOffer: SubscriptionOffer = JSON.parse(
+ session.metadata.subscriptionOffer ?? '{}'
+ );
+
+ if (subscriptionOffer) {
+ durationExtension = subscriptionOffer.durationExtension;
+ }
+
await this.createSubscription({
+ durationExtension,
price: session.amount_total / 100,
userId: session.client_reference_id
});
@@ -121,7 +168,7 @@ export class SubscriptionService {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
});
- let offer: SubscriptionOffer = price ? 'renewal' : 'default';
+ let offer: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) {
offer = 'renewal-early-bird-2023';
diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html
index b1285548..7560e15e 100644
--- a/apps/client/src/app/app.component.html
+++ b/apps/client/src/app/app.component.html
@@ -33,6 +33,7 @@
[deviceType]="deviceType"
[hasPermissionToChangeDateRange]="hasPermissionToChangeDateRange"
[hasPermissionToChangeFilters]="hasPermissionToChangeFilters"
+ [hasPromotion]="hasPromotion"
[hasTabs]="hasTabs"
[info]="info"
[pageTitle]="pageTitle"
diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts
index 75841686..86d4282a 100644
--- a/apps/client/src/app/app.component.ts
+++ b/apps/client/src/app/app.component.ts
@@ -57,6 +57,7 @@ export class AppComponent implements OnDestroy, OnInit {
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToChangeDateRange: boolean;
public hasPermissionToChangeFilters: boolean;
+ public hasPromotion = false;
public hasTabs = false;
public info: InfoItem;
public pageTitle: string;
@@ -136,6 +137,10 @@ export class AppComponent implements OnDestroy, OnInit {
permissions.enableFearAndGreedIndex
);
+ this.hasPromotion =
+ !!this.info?.subscriptionOffers?.default?.coupon ||
+ !!this.info?.subscriptionOffers?.default?.durationExtension;
+
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
@@ -231,6 +236,14 @@ export class AppComponent implements OnDestroy, OnInit {
this.hasInfoMessage =
this.canCreateAccount || !!this.user?.systemMessage;
+ this.hasPromotion =
+ !!this.info?.subscriptionOffers?.[
+ this.user?.subscription?.offer ?? 'default'
+ ]?.coupon ||
+ !!this.info?.subscriptionOffers?.[
+ this.user?.subscription?.offer ?? 'default'
+ ]?.durationExtension;
+
this.initializeTheme(this.user?.settings.colorScheme);
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html
index ff36c2eb..8a611d93 100644
--- a/apps/client/src/app/components/header/header.component.html
+++ b/apps/client/src/app/components/header/header.component.html
@@ -88,15 +88,20 @@
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
@@ -290,12 +295,17 @@
) {
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
Pricing
+
+ Pricing
+ @if (currentRoute !== routePricing && hasPromotion) {
+ %
+ }
+
+
}
@if (hasPermissionToAccessFearAndGreedIndex) {
diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts
index 33069aa2..1739d113 100644
--- a/apps/client/src/app/components/header/header.component.ts
+++ b/apps/client/src/app/components/header/header.component.ts
@@ -58,6 +58,7 @@ export class HeaderComponent implements OnChanges {
@Input() deviceType: string;
@Input() hasPermissionToChangeDateRange: boolean;
@Input() hasPermissionToChangeFilters: boolean;
+ @Input() hasPromotion: boolean;
@Input() hasTabs: boolean;
@Input() info: InfoItem;
@Input() pageTitle: string;
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
index 93bbe641..bde555d8 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
@@ -16,6 +16,7 @@ import {
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
+import { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@@ -31,6 +32,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
public coupon: number;
public couponId: string;
public defaultDateFormat: string;
+ public durationExtension: StringValue;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public price: number;
@@ -51,7 +53,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
private stripeService: StripeService,
private userService: UserService
) {
- const { baseCurrency, globalPermissions, subscriptions } =
+ const { baseCurrency, globalPermissions, subscriptionOffers } =
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
@@ -76,11 +78,18 @@ export class UserAccountMembershipComponent implements OnDestroy {
permissions.updateUserSettings
);
- this.coupon = subscriptions?.[this.user.subscription.offer]?.coupon;
+ this.coupon =
+ subscriptionOffers?.[this.user.subscription.offer]?.coupon;
this.couponId =
- subscriptions?.[this.user.subscription.offer]?.couponId;
- this.price = subscriptions?.[this.user.subscription.offer]?.price;
- this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
+ subscriptionOffers?.[this.user.subscription.offer]?.couponId;
+ this.durationExtension =
+ subscriptionOffers?.[
+ this.user.subscription.offer
+ ]?.durationExtension;
+ this.price =
+ subscriptionOffers?.[this.user.subscription.offer]?.price;
+ this.priceId =
+ subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck();
}
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.html b/apps/client/src/app/components/user-account-membership/user-account-membership.html
index d30ce7bd..82b329a6 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.html
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.html
@@ -34,6 +34,16 @@
per year
}
+ @if (durationExtension) {
+
+
+ Limited Offer! Get
+ {{ durationExtension }} extra
+
+
+ }
}
@if (!user?.subscription?.expiresAt) {
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 8bd0f1bd..f86a7590 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.component.ts
+++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts
@@ -6,6 +6,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { translate } from '@ghostfolio/ui/i18n';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { StringValue } from 'ms';
import { StripeService } from 'ngx-stripe';
import { Subject } from 'rxjs';
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
@@ -20,6 +21,7 @@ export class PricingPageComponent implements OnDestroy, OnInit {
public baseCurrency: string;
public coupon: number;
public couponId: string;
+ public durationExtension: StringValue;
public hasPermissionToUpdateUserSettings: boolean;
public importAndExportTooltipBasic = translate(
'DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC'
@@ -51,11 +53,12 @@ export class PricingPageComponent implements OnDestroy, OnInit {
) {}
public ngOnInit() {
- const { baseCurrency, subscriptions } = this.dataService.fetchInfo();
+ const { baseCurrency, subscriptionOffers } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
- this.coupon = subscriptions?.default?.coupon;
- this.price = subscriptions?.default?.price;
+ this.coupon = subscriptionOffers?.default?.coupon;
+ this.durationExtension = subscriptionOffers?.default?.durationExtension;
+ this.price = subscriptionOffers?.default?.price;
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
@@ -68,11 +71,18 @@ export class PricingPageComponent implements OnDestroy, OnInit {
permissions.updateUserSettings
);
- this.coupon = subscriptions?.[this.user?.subscription?.offer]?.coupon;
+ this.coupon =
+ subscriptionOffers?.[this.user?.subscription?.offer]?.coupon;
this.couponId =
- subscriptions?.[this.user.subscription.offer]?.couponId;
- this.price = subscriptions?.[this.user?.subscription?.offer]?.price;
- this.priceId = subscriptions?.[this.user.subscription.offer]?.priceId;
+ subscriptionOffers?.[this.user.subscription.offer]?.couponId;
+ this.durationExtension =
+ subscriptionOffers?.[
+ this.user?.subscription?.offer
+ ]?.durationExtension;
+ this.price =
+ subscriptionOffers?.[this.user?.subscription?.offer]?.price;
+ this.priceId =
+ subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck();
}
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html
index fe805ef6..605ad5d2 100644
--- a/apps/client/src/app/pages/pricing/pricing-page.html
+++ b/apps/client/src/app/pages/pricing/pricing-page.html
@@ -101,6 +101,11 @@
}
+ @if (durationExtension) {
+
+ }
@@ -159,6 +164,11 @@
}
+ @if (durationExtension) {
+
+ }
@@ -289,6 +299,14 @@
}
+ @if (durationExtension) {
+
+
+ Limited Offer! Get
+ {{ durationExtension }} extra
+
+
+ }
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
index ea14bbc6..39dbc481 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -35,9 +35,9 @@ export class GfProductPageComponent implements OnInit {
) {}
public ngOnInit() {
- const { subscriptions } = this.dataService.fetchInfo();
+ const { subscriptionOffers } = this.dataService.fetchInfo();
- this.price = subscriptions?.default?.price;
+ this.price = subscriptionOffers?.default?.price;
this.product1 = {
founded: 2021,
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 0ec04594..becc872d 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -46,7 +46,7 @@ import type { PortfolioPerformanceResponse } from './responses/portfolio-perform
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
import type { ScraperConfiguration } from './scraper-configuration.interface';
import type { Statistics } from './statistics.interface';
-import type { Subscription } from './subscription.interface';
+import type { SubscriptionOffer } from './subscription-offer.interface';
import type { SymbolMetrics } from './symbol-metrics.interface';
import type { SystemMessage } from './system-message.interface';
import type { TabConfiguration } from './tab-configuration.interface';
@@ -102,8 +102,8 @@ export {
ResponseError,
ScraperConfiguration,
Statistics,
+ SubscriptionOffer,
SystemMessage,
- Subscription,
SymbolMetrics,
TabConfiguration,
ToggleOption,
diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts
index 1b392633..bd3eb1f9 100644
--- a/libs/common/src/lib/interfaces/info-item.interface.ts
+++ b/libs/common/src/lib/interfaces/info-item.interface.ts
@@ -1,9 +1,9 @@
-import { SubscriptionOffer } from '@ghostfolio/common/types';
+import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { Platform, SymbolProfile } from '@prisma/client';
import { Statistics } from './statistics.interface';
-import { Subscription } from './subscription.interface';
+import { SubscriptionOffer } from './subscription-offer.interface';
export interface InfoItem {
baseCurrency: string;
@@ -18,5 +18,5 @@ export interface InfoItem {
platforms: Platform[];
statistics: Statistics;
stripePublicKey?: string;
- subscriptions: { [offer in SubscriptionOffer]: Subscription };
+ subscriptionOffers: { [offer in SubscriptionOfferKey]: SubscriptionOffer };
}
diff --git a/libs/common/src/lib/interfaces/subscription-offer.interface.ts b/libs/common/src/lib/interfaces/subscription-offer.interface.ts
new file mode 100644
index 00000000..8db91da6
--- /dev/null
+++ b/libs/common/src/lib/interfaces/subscription-offer.interface.ts
@@ -0,0 +1,9 @@
+import { StringValue } from 'ms';
+
+export interface SubscriptionOffer {
+ coupon?: number;
+ couponId?: string;
+ durationExtension?: StringValue;
+ price: number;
+ priceId: string;
+}
diff --git a/libs/common/src/lib/interfaces/subscription.interface.ts b/libs/common/src/lib/interfaces/subscription.interface.ts
deleted file mode 100644
index 29f5d3ab..00000000
--- a/libs/common/src/lib/interfaces/subscription.interface.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface Subscription {
- coupon?: number;
- couponId?: string;
- price: number;
- priceId: string;
-}
diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts
index 27cd1a61..647822d3 100644
--- a/libs/common/src/lib/interfaces/user.interface.ts
+++ b/libs/common/src/lib/interfaces/user.interface.ts
@@ -1,4 +1,4 @@
-import { SubscriptionOffer } from '@ghostfolio/common/types';
+import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Account, Tag } from '@prisma/client';
@@ -20,7 +20,7 @@ export interface User {
systemMessage?: SystemMessage;
subscription: {
expiresAt?: Date;
- offer: SubscriptionOffer;
+ offer: SubscriptionOfferKey;
type: SubscriptionType;
};
tags: (Tag & { isUsed: boolean })[];
diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts
index a66755ab..9e8178d3 100644
--- a/libs/common/src/lib/types/index.ts
+++ b/libs/common/src/lib/types/index.ts
@@ -15,7 +15,7 @@ import type { MarketState } from './market-state.type';
import type { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.type';
-import type { SubscriptionOffer } from './subscription-offer.type';
+import type { SubscriptionOfferKey } from './subscription-offer-key.type';
import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type';
@@ -37,7 +37,7 @@ export type {
MarketState,
OrderWithAccount,
RequestWithUser,
- SubscriptionOffer,
+ SubscriptionOfferKey,
UserWithSettings,
ViewMode
};
diff --git a/libs/common/src/lib/types/subscription-offer.type.ts b/libs/common/src/lib/types/subscription-offer-key.type.ts
similarity index 71%
rename from libs/common/src/lib/types/subscription-offer.type.ts
rename to libs/common/src/lib/types/subscription-offer-key.type.ts
index 98977da4..f6d898a0 100644
--- a/libs/common/src/lib/types/subscription-offer.type.ts
+++ b/libs/common/src/lib/types/subscription-offer-key.type.ts
@@ -1,4 +1,4 @@
-export type SubscriptionOffer =
+export type SubscriptionOfferKey =
| 'default'
| 'renewal'
| 'renewal-early-bird-2023'
diff --git a/libs/common/src/lib/types/user-with-settings.type.ts b/libs/common/src/lib/types/user-with-settings.type.ts
index 59e9f142..2a669d26 100644
--- a/libs/common/src/lib/types/user-with-settings.type.ts
+++ b/libs/common/src/lib/types/user-with-settings.type.ts
@@ -1,5 +1,5 @@
import { UserSettings } from '@ghostfolio/common/interfaces';
-import { SubscriptionOffer } from '@ghostfolio/common/types';
+import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Access, Account, Settings, User } from '@prisma/client';
@@ -13,7 +13,7 @@ export type UserWithSettings = User & {
Settings: Settings & { settings: UserSettings };
subscription?: {
expiresAt?: Date;
- offer: SubscriptionOffer;
+ offer: SubscriptionOfferKey;
type: SubscriptionType;
};
};
From a80ca507f881b20fd7510893835eb733bf369ece Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:45:26 +0100
Subject: [PATCH 12/65] Feature/add lastRequestAt to analytics (#4010)
* Add lastRequestAt to Analytics
---
apps/api/src/app/admin/admin.service.ts | 4 ++--
apps/api/src/app/auth/jwt.strategy.ts | 2 +-
apps/api/src/app/info/info.service.ts | 2 +-
.../migration.sql | 5 +++++
prisma/schema.prisma | 2 ++
5 files changed, 11 insertions(+), 4 deletions(-)
create mode 100644 prisma/migrations/20241102121004_added_last_request_at_to_analytics/migration.sql
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index 860f1985..49964c77 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -641,7 +641,7 @@ export class AdminService {
}
private async getUsersWithAnalytics(): Promise {
- let orderBy: any = {
+ let orderBy: Prisma.UserOrderByWithRelationInput = {
createdAt: 'desc'
};
let where: Prisma.UserWhereInput;
@@ -649,7 +649,7 @@ export class AdminService {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
orderBy = {
Analytics: {
- updatedAt: 'desc'
+ lastRequestAt: 'desc'
}
};
where = {
diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts
index a8ad8fd0..1200ed46 100644
--- a/apps/api/src/app/auth/jwt.strategy.ts
+++ b/apps/api/src/app/auth/jwt.strategy.ts
@@ -46,7 +46,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
update: {
country,
activityCount: { increment: 1 },
- updatedAt: new Date()
+ lastRequestAt: new Date()
},
where: { userId: user.id }
});
diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts
index f81ddd71..62a78d1d 100644
--- a/apps/api/src/app/info/info.service.ts
+++ b/apps/api/src/app/info/info.service.ts
@@ -142,7 +142,7 @@ export class InfoService {
},
{
Analytics: {
- updatedAt: {
+ lastRequestAt: {
gt: subDays(new Date(), aDays)
}
}
diff --git a/prisma/migrations/20241102121004_added_last_request_at_to_analytics/migration.sql b/prisma/migrations/20241102121004_added_last_request_at_to_analytics/migration.sql
new file mode 100644
index 00000000..b7af3103
--- /dev/null
+++ b/prisma/migrations/20241102121004_added_last_request_at_to_analytics/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "Analytics" ADD COLUMN "lastRequestAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+-- CreateIndex
+CREATE INDEX "Analytics_lastRequestAt_idx" ON "Analytics"("lastRequestAt");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7f386a71..5a34e8e1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -68,10 +68,12 @@ model Analytics {
activityCount Int @default(0)
country String?
dataProviderGhostfolioDailyRequests Int @default(0)
+ lastRequestAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String @id
User User @relation(fields: [userId], onDelete: Cascade, references: [id])
+ @@index([lastRequestAt])
@@index([updatedAt])
}
From 5652d19a88d81faeffac8b4c7e8a44521b172d79 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:45:47 +0100
Subject: [PATCH 13/65] Feature/upgrade stripe dependencies 20241102 (#4009)
* Upgrade stripe dependencies
* Update changelog
---
CHANGELOG.md | 1 +
.../app/subscription/subscription.service.ts | 2 +-
package-lock.json | 30 +++++++++----------
package.json | 6 ++--
4 files changed, 20 insertions(+), 19 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 397cc998..43238001 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Reverted the permissions (`chmod 0700`) on `entrypoint.sh` in the `Dockerfile`
+- Upgraded the _Stripe_ dependencies
## 2.120.0 - 2024-10-30
diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts
index 47e6db00..ef73f346 100644
--- a/apps/api/src/app/subscription/subscription.service.ts
+++ b/apps/api/src/app/subscription/subscription.service.ts
@@ -31,7 +31,7 @@ export class SubscriptionService {
this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'),
{
- apiVersion: '2024-04-10'
+ apiVersion: '2024-09-30.acacia'
}
);
}
diff --git a/package-lock.json b/package-lock.json
index 0761fdf9..3e606f03 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.119.0",
+ "version": "2.120.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.119.0",
+ "version": "2.120.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -43,7 +43,7 @@
"@prisma/client": "5.21.1",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
- "@stripe/stripe-js": "3.5.0",
+ "@stripe/stripe-js": "4.9.0",
"alphavantage": "2.2.0",
"big.js": "6.2.1",
"body-parser": "1.20.2",
@@ -78,7 +78,7 @@
"ngx-device-detector": "8.0.0",
"ngx-markdown": "18.0.0",
"ngx-skeleton-loader": "7.0.0",
- "ngx-stripe": "18.0.0",
+ "ngx-stripe": "18.1.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
"passport": "0.7.0",
@@ -86,7 +86,7 @@
"passport-jwt": "4.0.1",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
- "stripe": "15.11.0",
+ "stripe": "17.3.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
"uuid": "9.0.1",
@@ -10334,9 +10334,9 @@
}
},
"node_modules/@stripe/stripe-js": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.5.0.tgz",
- "integrity": "sha512-pKS3wZnJoL1iTyGBXAvCwduNNeghJHY6QSRSNNvpYnrrQrLZ6Owsazjyynu0e0ObRgks0i7Rv+pe2M7/MBTZpQ==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.9.0.tgz",
+ "integrity": "sha512-tMPZQZZXGWyNX7hbgenq+1xEj2oigJ54XddbtSX36VedoKsPBq7dxwRXu4Xd5FdpT3JDyyDtnmvYkaSnH1yHTQ==",
"license": "MIT",
"engines": {
"node": ">=12.16"
@@ -26872,9 +26872,9 @@
}
},
"node_modules/ngx-stripe": {
- "version": "18.0.0",
- "resolved": "https://registry.npmjs.org/ngx-stripe/-/ngx-stripe-18.0.0.tgz",
- "integrity": "sha512-AT67vLeqEUDMnK5TfEaorumYJyOWqecbrh/1UWNtN8vF6Yzb0L/Dty3ANAa/QQi0OvBg6gXrudrhEnT8pT5lng==",
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/ngx-stripe/-/ngx-stripe-18.1.0.tgz",
+ "integrity": "sha512-fNWmFaCWWzfsr8GU9Bmi6fwgHZHMI9UwpV5M0HMvkANnz9n7JWjP2Uck6zk0lXdu9q989aIbqj4awbLCZk/TUw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -26882,7 +26882,7 @@
"peerDependencies": {
"@angular/common": ">=18.0.0 <19.0.0",
"@angular/core": ">=18.0.0 <19.0.0",
- "@stripe/stripe-js": ">=3.0.0 <4.0.0"
+ "@stripe/stripe-js": ">=4.0.0 <5.0.0"
}
},
"node_modules/nice-napi": {
@@ -31883,9 +31883,9 @@
}
},
"node_modules/stripe": {
- "version": "15.11.0",
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-15.11.0.tgz",
- "integrity": "sha512-qmZF0PN1jRVpiQrXL8eTb9Jy/6S+aUlcDquKBFT2h3PkaD7RZ444FIojVXUg67FK2zFIUNXgMv02c7csdL5qHg==",
+ "version": "17.3.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.3.0.tgz",
+ "integrity": "sha512-WACmytj1MssbIwGwPfAomo61jgldb2B/cB6A3W/Bqs9zId1olVcAa8X7HERkqpw4190GSsbvrD7KnkZogatyvw==",
"license": "MIT",
"dependencies": {
"@types/node": ">=8.1.0",
diff --git a/package.json b/package.json
index 134d72f9..0cb2a473 100644
--- a/package.json
+++ b/package.json
@@ -89,7 +89,7 @@
"@prisma/client": "5.21.1",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
- "@stripe/stripe-js": "3.5.0",
+ "@stripe/stripe-js": "4.9.0",
"alphavantage": "2.2.0",
"big.js": "6.2.1",
"body-parser": "1.20.2",
@@ -124,7 +124,7 @@
"ngx-device-detector": "8.0.0",
"ngx-markdown": "18.0.0",
"ngx-skeleton-loader": "7.0.0",
- "ngx-stripe": "18.0.0",
+ "ngx-stripe": "18.1.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
"passport": "0.7.0",
@@ -132,7 +132,7 @@
"passport-jwt": "4.0.1",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
- "stripe": "15.11.0",
+ "stripe": "17.3.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
"uuid": "9.0.1",
From de74d5c3d6ce859b752640ef77d6192828561ece Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:48:03 +0100
Subject: [PATCH 14/65] Release 2.121.0 (#4011)
---
CHANGELOG.md | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43238001..89705c83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## 2.121.0 - 2024-11-02
### Added
diff --git a/package-lock.json b/package-lock.json
index 3e606f03..861cf797 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.120.0",
+ "version": "2.121.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.120.0",
+ "version": "2.121.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 0cb2a473..53d5db44 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.120.0",
+ "version": "2.121.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From b2aa31f4ba57d6fb3d371108755ddc539cb257d3 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 22:28:13 +0100
Subject: [PATCH 15/65] Bugfix/handle missing Stripe api key exception (#4013)
* Conditionally initialize Stripe
---
.../src/app/subscription/subscription.service.ts | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts
index ef73f346..ae0260d8 100644
--- a/apps/api/src/app/subscription/subscription.service.ts
+++ b/apps/api/src/app/subscription/subscription.service.ts
@@ -28,12 +28,14 @@ export class SubscriptionService {
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService
) {
- this.stripe = new Stripe(
- this.configurationService.get('STRIPE_SECRET_KEY'),
- {
- apiVersion: '2024-09-30.acacia'
- }
- );
+ if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
+ this.stripe = new Stripe(
+ this.configurationService.get('STRIPE_SECRET_KEY'),
+ {
+ apiVersion: '2024-09-30.acacia'
+ }
+ );
+ }
}
public async createCheckoutSession({
From c857bd1b2e51aff7eb83e667b5f6b85ac41a604f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 2 Nov 2024 22:29:28 +0100
Subject: [PATCH 16/65] Release 2.121.1 (#4014)
---
CHANGELOG.md | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89705c83..5fff96a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ 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).
-## 2.121.0 - 2024-11-02
+## 2.121.1 - 2024-11-02
### Added
diff --git a/package-lock.json b/package-lock.json
index 861cf797..68e0a178 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.121.0",
+ "version": "2.121.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.121.0",
+ "version": "2.121.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 53d5db44..f3c7ad67 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.121.0",
+ "version": "2.121.1",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From a1fbdc2ebea32d3dab258014681e63b8e3af09d9 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 3 Nov 2024 15:51:59 +0100
Subject: [PATCH 17/65] Bugfix/exception handling in user authorization (#4015)
* Add guard
* Update changelog
---
CHANGELOG.md | 6 ++++++
apps/api/src/app/auth/jwt.strategy.ts | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5fff96a3..f675e03d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Fixed
+
+- Improved the exception handling in the user authorization service
+
## 2.121.1 - 2024-11-02
### Added
diff --git a/apps/api/src/app/auth/jwt.strategy.ts b/apps/api/src/app/auth/jwt.strategy.ts
index 1200ed46..7a3fb224 100644
--- a/apps/api/src/app/auth/jwt.strategy.ts
+++ b/apps/api/src/app/auth/jwt.strategy.ts
@@ -60,7 +60,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
);
}
} catch (error) {
- if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) {
+ if (error?.getStatus?.() === StatusCodes.TOO_MANY_REQUESTS) {
throw error;
} else {
throw new HttpException(
From 93001b68ad0efbc79f2bf0b26ed34e6715950a95 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 3 Nov 2024 19:58:19 +0100
Subject: [PATCH 18/65] Feature/introduce lookup response interface (#4019)
* Introduce lookup response interface
* Refactor lookup item interface
---
apps/api/src/app/symbol/symbol.controller.ts | 4 ++--
apps/api/src/app/symbol/symbol.service.ts | 10 ++++++----
.../alpha-vantage/alpha-vantage.service.ts | 10 +++++-----
.../data-provider/coingecko/coingecko.service.ts | 11 ++++++-----
.../services/data-provider/data-provider.service.ts | 11 +++++++----
.../eod-historical-data.service.ts | 11 ++++++-----
.../financial-modeling-prep.service.ts | 11 ++++++-----
.../google-sheets/google-sheets.service.ts | 10 +++++-----
.../interfaces/data-provider.interface.ts | 11 +++++------
.../services/data-provider/manual/manual.service.ts | 6 ++----
.../data-provider/rapid-api/rapid-api.service.ts | 8 +++++---
.../yahoo-finance/yahoo-finance.service.ts | 9 ++++++---
apps/client/src/app/services/data.service.ts | 4 ++--
libs/common/src/lib/interfaces/index.ts | 4 ++++
.../src/lib}/interfaces/lookup-item.interface.ts | 4 ++--
.../interfaces/responses/lookup-response.interface.ts | 5 +++++
.../symbol-autocomplete.component.ts | 2 +-
17 files changed, 75 insertions(+), 56 deletions(-)
rename {apps/api/src/app/symbol => libs/common/src/lib}/interfaces/lookup-item.interface.ts (80%)
create mode 100644 libs/common/src/lib/interfaces/responses/lookup-response.interface.ts
diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts
index b3b9dc10..89cdd416 100644
--- a/apps/api/src/app/symbol/symbol.controller.ts
+++ b/apps/api/src/app/symbol/symbol.controller.ts
@@ -2,6 +2,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
+import { LookupResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@@ -21,7 +22,6 @@ import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isDate, isEmpty } from 'lodash';
-import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
import { SymbolService } from './symbol.service';
@@ -41,7 +41,7 @@ export class SymbolController {
public async lookupSymbol(
@Query('includeIndices') includeIndicesParam = 'false',
@Query('query') query = ''
- ): Promise<{ items: LookupItem[] }> {
+ ): Promise {
const includeIndices = includeIndicesParam === 'true';
try {
diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts
index 2baca18d..ae864e2f 100644
--- a/apps/api/src/app/symbol/symbol.service.ts
+++ b/apps/api/src/app/symbol/symbol.service.ts
@@ -5,13 +5,15 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
+import {
+ HistoricalDataItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { format, subDays } from 'date-fns';
-import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@Injectable()
@@ -104,8 +106,8 @@ export class SymbolService {
includeIndices?: boolean;
query: string;
user: UserWithSettings;
- }): Promise<{ items: LookupItem[] }> {
- const results: { items: LookupItem[] } = { items: [] };
+ }): Promise {
+ const results: LookupResponse = { items: [] };
if (!query) {
return results;
diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
index 01658494..5c9eee12 100644
--- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
+++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -12,7 +11,10 @@ import {
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -119,9 +121,7 @@ export class AlphaVantageService implements DataProviderInterface {
return undefined;
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const result = await this.alphaVantage.data.search(query);
return {
diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
index d420c51f..7d6f22c6 100644
--- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
+++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import {
@@ -221,9 +224,7 @@ export class CoinGeckoService implements DataProviderInterface {
return 'bitcoin';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items: LookupItem[] = [];
try {
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 385c7b07..c4670bc3 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -1,5 +1,4 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import {
@@ -20,7 +19,11 @@ import {
getStartOfUtcDate,
isDerivedCurrency
} from '@ghostfolio/common/helper';
-import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
+import {
+ AssetProfileIdentifier,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common';
@@ -571,8 +574,8 @@ export class DataProviderService {
includeIndices?: boolean;
query: string;
user: UserWithSettings;
- }): Promise<{ items: LookupItem[] }> {
- const promises: Promise<{ items: LookupItem[] }>[] = [];
+ }): Promise {
+ const promises: Promise[] = [];
let lookupItems: LookupItem[] = [];
if (query?.length < 2) {
diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
index c3c948b4..7329b821 100644
--- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
+++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -17,7 +16,11 @@ import {
REPLACE_NAME_PARTS
} from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { MarketState } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
@@ -317,9 +320,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
return 'AAPL.US';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const searchResult = await this.getSearchResult(query);
return {
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 7d5b3847..9334fc4c 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,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -169,9 +172,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return 'AAPL';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items: LookupItem[] = [];
try {
diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
index 9f234423..f18d670d 100644
--- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
+++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -14,7 +13,10 @@ import {
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -157,9 +159,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return 'INDEXSP:.INX';
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
const items = await this.prismaService.symbolProfile.findMany({
select: {
assetClass: true,
diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
index 3b364447..7352ce78 100644
--- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
+++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
@@ -1,9 +1,11 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import {
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -44,10 +46,7 @@ export interface DataProviderInterface {
getTestSymbol(): string;
- search({
- includeIndices,
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }>;
+ search({ includeIndices, query }: GetSearchParams): Promise;
}
export interface GetDividendsParams {
diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts
index 030ab8ea..30c7efa6 100644
--- a/apps/api/src/services/data-provider/manual/manual.service.ts
+++ b/apps/api/src/services/data-provider/manual/manual.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -20,6 +19,7 @@ import {
} from '@ghostfolio/common/helper';
import {
DataProviderInfo,
+ LookupResponse,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
@@ -219,9 +219,7 @@ export class ManualService implements DataProviderInterface {
return undefined;
}
- public async search({
- query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({ query }: GetSearchParams): Promise {
let items = await this.prismaService.symbolProfile.findMany({
select: {
assetClass: true,
diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
index e47e96d8..29e7f4ee 100644
--- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
+++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
@@ -13,7 +12,10 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -121,7 +123,7 @@ export class RapidApiService implements DataProviderInterface {
return undefined;
}
- public async search({}: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ public async search({}: GetSearchParams): Promise {
return { items: [] };
}
diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
index 2d67c646..27da18ab 100644
--- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
+++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
@@ -1,4 +1,3 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import {
@@ -14,7 +13,11 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
+import {
+ DataProviderInfo,
+ LookupItem,
+ LookupResponse
+} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
@@ -224,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
public async search({
includeIndices = false,
query
- }: GetSearchParams): Promise<{ items: LookupItem[] }> {
+ }: GetSearchParams): Promise {
const items: LookupItem[] = [];
try {
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index abf4b21a..1d40de69 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -10,7 +10,6 @@ import {
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface';
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
@@ -30,6 +29,7 @@ import {
Filter,
ImportResponse,
InfoItem,
+ LookupResponse,
OAuthResponse,
PortfolioDetails,
PortfolioDividends,
@@ -464,7 +464,7 @@ export class DataService {
}
return this.http
- .get<{ items: LookupItem[] }>('/api/v1/symbol/lookup', { params })
+ .get('/api/v1/symbol/lookup', { params })
.pipe(
map((respose) => {
return respose.items;
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index becc872d..eca14706 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -23,6 +23,7 @@ import type { Holding } from './holding.interface';
import type { InfoItem } from './info-item.interface';
import type { InvestmentItem } from './investment-item.interface';
import type { LineChartItem } from './line-chart-item.interface';
+import type { LookupItem } from './lookup-item.interface';
import type { PortfolioChart } from './portfolio-chart.interface';
import type { PortfolioDetails } from './portfolio-details.interface';
import type { PortfolioDividends } from './portfolio-dividends.interface';
@@ -40,6 +41,7 @@ import type { AccountBalancesResponse } from './responses/account-balances-respo
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { ResponseError } from './responses/errors.interface';
import type { ImportResponse } from './responses/import-response.interface';
+import type { LookupResponse } from './responses/lookup-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
@@ -82,6 +84,8 @@ export {
InfoItem,
InvestmentItem,
LineChartItem,
+ LookupItem,
+ LookupResponse,
OAuthResponse,
PortfolioChart,
PortfolioDetails,
diff --git a/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts b/libs/common/src/lib/interfaces/lookup-item.interface.ts
similarity index 80%
rename from apps/api/src/app/symbol/interfaces/lookup-item.interface.ts
rename to libs/common/src/lib/interfaces/lookup-item.interface.ts
index 89b47639..fa91ed69 100644
--- a/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts
+++ b/libs/common/src/lib/interfaces/lookup-item.interface.ts
@@ -1,7 +1,7 @@
-import { DataProviderInfo } from '@ghostfolio/common/interfaces';
-
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
+import { DataProviderInfo } from './data-provider-info.interface';
+
export interface LookupItem {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
diff --git a/libs/common/src/lib/interfaces/responses/lookup-response.interface.ts b/libs/common/src/lib/interfaces/responses/lookup-response.interface.ts
new file mode 100644
index 00000000..579be9d0
--- /dev/null
+++ b/libs/common/src/lib/interfaces/responses/lookup-response.interface.ts
@@ -0,0 +1,5 @@
+import { LookupItem } from '../lookup-item.interface';
+
+export interface LookupResponse {
+ items: LookupItem[];
+}
diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
index da97aac0..a537c50a 100644
--- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
+++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
@@ -1,6 +1,6 @@
-import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { DataService } from '@ghostfolio/client/services/data.service';
+import { LookupItem } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
From 316b7e82f1a9a8e03819e8339456826c7bfbe3e1 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 4 Nov 2024 19:05:01 +0100
Subject: [PATCH 19/65] Feature/upgrade countries-list to version 3.1.1 (#4020)
* Upgrade countries-list to version 3.1.1
* Update changelog
---
CHANGELOG.md | 4 ++++
package-lock.json | 8 ++++----
package.json | 2 +-
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f675e03d..2ae5de6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Changed
+
+- Upgraded `countries-list` from version `3.1.0` to `3.1.1`
+
### Fixed
- Improved the exception handling in the user authorization service
diff --git a/package-lock.json b/package-lock.json
index 68e0a178..9dfb1f59 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -61,7 +61,7 @@
"class-validator": "0.14.1",
"color": "4.2.3",
"countries-and-timezones": "3.4.1",
- "countries-list": "3.1.0",
+ "countries-list": "3.1.1",
"countup.js": "2.8.0",
"date-fns": "3.6.0",
"envalid": "7.3.1",
@@ -15243,9 +15243,9 @@
}
},
"node_modules/countries-list": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.0.tgz",
- "integrity": "sha512-HpTBLZba1VPTZSjUnUwR7SykxV7Z/7/+ZM5x5wi5tO99Qvom6bE2SC+AQ18016ujg3jSlYBbMITrHNnPAHSM9Q==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz",
+ "integrity": "sha512-nPklKJ5qtmY5MdBKw1NiBAoyx5Sa7p2yPpljZyQ7gyCN1m+eMFs9I6CT37Mxt8zvR5L3VzD3DJBE4WQzX3WF4A==",
"license": "MIT"
},
"node_modules/countup.js": {
diff --git a/package.json b/package.json
index f3c7ad67..06bee046 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
"class-validator": "0.14.1",
"color": "4.2.3",
"countries-and-timezones": "3.4.1",
- "countries-list": "3.1.0",
+ "countries-list": "3.1.1",
"countup.js": "2.8.0",
"date-fns": "3.6.0",
"envalid": "7.3.1",
From 9c27fb70aaaef5abe225aef024a65b9fcbcab652 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 5 Nov 2024 13:01:53 +0100
Subject: [PATCH 20/65] Feature/minor improvements in data provider service
(#4017)
* Refactoring
---
.../api/src/services/data-provider/data-provider.service.ts | 6 +++---
apps/client/src/app/services/data.service.ts | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
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 c4670bc3..bf23f96c 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -575,8 +575,8 @@ export class DataProviderService {
query: string;
user: UserWithSettings;
}): Promise {
- const promises: Promise[] = [];
let lookupItems: LookupItem[] = [];
+ const promises: Promise[] = [];
if (query?.length < 2) {
return { items: lookupItems };
@@ -606,9 +606,9 @@ export class DataProviderService {
});
const filteredItems = lookupItems
- .filter((lookupItem) => {
+ .filter(({ currency }) => {
// Only allow symbols with supported currency
- return lookupItem.currency ? true : false;
+ return currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 1d40de69..cde7555b 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -466,8 +466,8 @@ export class DataService {
return this.http
.get('/api/v1/symbol/lookup', { params })
.pipe(
- map((respose) => {
- return respose.items;
+ map(({ items }) => {
+ return items;
})
);
}
From 04d5416c6ded25d53818bc890e599852d16ea1d2 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 6 Nov 2024 18:09:44 +0100
Subject: [PATCH 21/65] Feature/harmonize date formats in import test files
(#3910)
* Harmonize date formats
---
test/import/ok-derived-currency.json | 2 +-
test/import/ok-vti-buy-long-history.json | 6 +++---
test/import/ok-without-accounts.json | 8 ++++----
test/import/ok.json | 10 +++++-----
test/import/unavailable-exchange-rate.json | 2 +-
5 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/test/import/ok-derived-currency.json b/test/import/ok-derived-currency.json
index b43be395..fe5554be 100644
--- a/test/import/ok-derived-currency.json
+++ b/test/import/ok-derived-currency.json
@@ -23,7 +23,7 @@
"unitPrice": 10875.00,
"currency": "ZAc",
"dataSource": "YAHOO",
- "date": "2024-06-27T22:00:00.000Z",
+ "date": "2024-06-28T00:00:00.000Z",
"symbol": "JSE.JO"
}
]
diff --git a/test/import/ok-vti-buy-long-history.json b/test/import/ok-vti-buy-long-history.json
index e6020e40..1586cff7 100644
--- a/test/import/ok-vti-buy-long-history.json
+++ b/test/import/ok-vti-buy-long-history.json
@@ -11,7 +11,7 @@
"unitPrice": 65.31,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2012-01-02T22:00:00.000Z",
+ "date": "2012-01-03T00:00:00.000Z",
"symbol": "VTI"
},
{
@@ -21,7 +21,7 @@
"unitPrice": 65.40,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2011-01-02T22:00:00.000Z",
+ "date": "2011-01-03T00:00:00.000Z",
"symbol": "VTI"
},
{
@@ -31,7 +31,7 @@
"unitPrice": 57.05,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2010-01-03T22:00:00.000Z",
+ "date": "2010-01-04T00:00:00.000Z",
"symbol": "VTI"
}
]
diff --git a/test/import/ok-without-accounts.json b/test/import/ok-without-accounts.json
index 63961be7..2ba0925b 100644
--- a/test/import/ok-without-accounts.json
+++ b/test/import/ok-without-accounts.json
@@ -11,7 +11,7 @@
"unitPrice": 0,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2050-06-05T22:00:00.000Z",
+ "date": "2050-06-06T00:00:00.000Z",
"symbol": "MSFT"
},
{
@@ -21,7 +21,7 @@
"unitPrice": 500000,
"currency": "USD",
"dataSource": "MANUAL",
- "date": "2021-12-31T22:00:00.000Z",
+ "date": "2022-01-01T00:00:00.000Z",
"symbol": "Penthouse Apartment"
},
{
@@ -31,7 +31,7 @@
"unitPrice": 0.62,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2021-11-16T22:00:00.000Z",
+ "date": "2021-11-17T00:00:00.000Z",
"symbol": "MSFT"
},
{
@@ -41,7 +41,7 @@
"unitPrice": 298.58,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2021-09-15T22:00:00.000Z",
+ "date": "2021-09-16T00:00:00.000Z",
"symbol": "MSFT"
}
]
diff --git a/test/import/ok.json b/test/import/ok.json
index 0af28550..4bce98ba 100644
--- a/test/import/ok.json
+++ b/test/import/ok.json
@@ -23,7 +23,7 @@
"unitPrice": 0,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2050-06-05T22:00:00.000Z",
+ "date": "2050-06-06T00:00:00.000Z",
"symbol": "US5949181045"
},
{
@@ -35,7 +35,7 @@
"unitPrice": 500000,
"currency": "USD",
"dataSource": "MANUAL",
- "date": "2021-12-31T22:00:00.000Z",
+ "date": "2022-01-01T00:00:00.000Z",
"symbol": "Penthouse Apartment"
},
{
@@ -47,7 +47,7 @@
"unitPrice": 0.62,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2021-11-16T22:00:00.000Z",
+ "date": "2021-11-17T00:00:00.000Z",
"symbol": "MSFT"
},
{
@@ -59,7 +59,7 @@
"unitPrice": 298.58,
"currency": "USD",
"dataSource": "YAHOO",
- "date": "2021-09-15T22:00:00.000Z",
+ "date": "2021-09-16T00:00:00.000Z",
"symbol": "MSFT"
},
{
@@ -71,7 +71,7 @@
"unitPrice": 0,
"currency": "USD",
"dataSource": "MANUAL",
- "date": "2021-08-31T22:00:00.000Z",
+ "date": "2021-09-01T00:00:00.000Z",
"symbol": "Account Opening Fee"
}
]
diff --git a/test/import/unavailable-exchange-rate.json b/test/import/unavailable-exchange-rate.json
index 2d21a76c..4d8be156 100644
--- a/test/import/unavailable-exchange-rate.json
+++ b/test/import/unavailable-exchange-rate.json
@@ -12,7 +12,7 @@
"unitPrice": 0,
"currency": "EUR",
"dataSource": "YAHOO",
- "date": "1990-01-01T22:00:00.000Z",
+ "date": "1990-01-01T00:00:00.000Z",
"symbol": "MSFT"
}
]
From 190bb4b6fbaa468ee4d9d1455225a9175bea68ca Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 7 Nov 2024 19:57:37 +0100
Subject: [PATCH 22/65] Feature/refactor data gathering items (#4032)
* Refactoring
---
.../src/app/portfolio/current-rate.service.ts | 21 ++++-----
.../src/app/portfolio/portfolio.service.ts | 16 ++++---
apps/api/src/app/symbol/symbol.service.ts | 2 +-
.../data-provider/data-provider.service.ts | 47 +++++++++++--------
.../data-gathering.processor.ts | 2 +-
.../data-gathering/data-gathering.service.ts | 2 +-
6 files changed, 49 insertions(+), 41 deletions(-)
diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts
index cd199482..ab7bf2eb 100644
--- a/apps/api/src/app/portfolio/current-rate.service.ts
+++ b/apps/api/src/app/portfolio/current-rate.service.ts
@@ -52,27 +52,24 @@ export class CurrentRateService {
.then((dataResultProvider) => {
const result: GetValueObject[] = [];
- for (const dataGatheringItem of dataGatheringItems) {
- if (
- dataResultProvider?.[dataGatheringItem.symbol]?.dataProviderInfo
- ) {
+ for (const { dataSource, symbol } of dataGatheringItems) {
+ if (dataResultProvider?.[symbol]?.dataProviderInfo) {
dataProviderInfos.push(
- dataResultProvider[dataGatheringItem.symbol].dataProviderInfo
+ dataResultProvider[symbol].dataProviderInfo
);
}
- if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
+ if (dataResultProvider?.[symbol]?.marketPrice) {
result.push({
- dataSource: dataGatheringItem.dataSource,
+ dataSource,
+ symbol,
date: today,
- marketPrice:
- dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice,
- symbol: dataGatheringItem.symbol
+ marketPrice: dataResultProvider?.[symbol]?.marketPrice
});
} else {
quoteErrors.push({
- dataSource: dataGatheringItem.dataSource,
- symbol: dataGatheringItem.symbol
+ dataSource,
+ symbol
});
}
}
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index d4018686..15ca227e 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -413,15 +413,16 @@ export class PortfolioService {
);
}
- const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
+ const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
};
});
- const symbolProfiles =
- await this.symbolProfileService.getSymbolProfiles(dataGatheringItems);
+ const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
+ assetProfileIdentifiers
+ );
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
for (const symbolProfile of symbolProfiles) {
@@ -848,7 +849,7 @@ export class PortfolioService {
if (isEmpty(historicalData)) {
try {
historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [
+ assetProfileIdentifiers: [
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
],
from: portfolioStart,
@@ -953,7 +954,7 @@ export class PortfolioService {
return !quantity.eq(0);
});
- const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
+ const assetProfileIdentifiers = positions.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
@@ -961,7 +962,10 @@ export class PortfolioService {
});
const [dataProviderResponses, symbolProfiles] = await Promise.all([
- this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
+ this.dataProviderService.getQuotes({
+ user,
+ items: assetProfileIdentifiers
+ }),
this.symbolProfileService.getSymbolProfiles(
positions.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts
index ae864e2f..56befb9b 100644
--- a/apps/api/src/app/symbol/symbol.service.ts
+++ b/apps/api/src/app/symbol/symbol.service.ts
@@ -86,7 +86,7 @@ export class SymbolService {
try {
historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: date,
to: date
});
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 bf23f96c..c8a7422d 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -91,11 +91,11 @@ export class DataProviderService {
const promises = [];
- for (const [dataSource, dataGatheringItems] of Object.entries(
+ for (const [dataSource, assetProfileIdentifiers] of Object.entries(
itemsGroupedByDataSource
)) {
- const symbols = dataGatheringItems.map((dataGatheringItem) => {
- return dataGatheringItem.symbol;
+ const symbols = assetProfileIdentifiers.map(({ symbol }) => {
+ return symbol;
});
for (const symbol of symbols) {
@@ -242,11 +242,11 @@ export class DataProviderService {
}
public async getHistoricalRaw({
- dataGatheringItems,
+ assetProfileIdentifiers,
from,
to
}: {
- dataGatheringItems: AssetProfileIdentifier[];
+ assetProfileIdentifiers: AssetProfileIdentifier[];
from: Date;
to: Date;
}): Promise<{
@@ -255,25 +255,32 @@ export class DataProviderService {
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
if (
this.hasCurrency({
- dataGatheringItems,
+ assetProfileIdentifiers,
currency: `${DEFAULT_CURRENCY}${currency}`
})
) {
// Skip derived currency
- dataGatheringItems = dataGatheringItems.filter(({ symbol }) => {
- return symbol !== `${DEFAULT_CURRENCY}${currency}`;
- });
+ assetProfileIdentifiers = assetProfileIdentifiers.filter(
+ ({ symbol }) => {
+ return symbol !== `${DEFAULT_CURRENCY}${currency}`;
+ }
+ );
// Add root currency
- dataGatheringItems.push({
+ assetProfileIdentifiers.push({
dataSource: this.getDataSourceForExchangeRates(),
symbol: `${DEFAULT_CURRENCY}${rootCurrency}`
});
}
}
- dataGatheringItems = uniqWith(dataGatheringItems, (obj1, obj2) => {
- return obj1.dataSource === obj2.dataSource && obj1.symbol === obj2.symbol;
- });
+ assetProfileIdentifiers = uniqWith(
+ assetProfileIdentifiers,
+ (obj1, obj2) => {
+ return (
+ obj1.dataSource === obj2.dataSource && obj1.symbol === obj2.symbol
+ );
+ }
+ );
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
@@ -283,7 +290,7 @@ export class DataProviderService {
data: { [date: string]: IDataProviderHistoricalResponse };
symbol: string;
}>[] = [];
- for (const { dataSource, symbol } of dataGatheringItems) {
+ for (const { dataSource, symbol } of assetProfileIdentifiers) {
const dataProvider = this.getDataProvider(dataSource);
if (dataProvider.canHandle(symbol)) {
if (symbol === `${DEFAULT_CURRENCY}USX`) {
@@ -418,7 +425,7 @@ export class DataProviderService {
const promises: Promise[] = [];
- for (const [dataSource, dataGatheringItems] of Object.entries(
+ for (const [dataSource, assetProfileIdentifiers] of Object.entries(
itemsGroupedByDataSource
)) {
const dataProvider = this.getDataProvider(DataSource[dataSource]);
@@ -431,7 +438,7 @@ export class DataProviderService {
continue;
}
- const symbols = dataGatheringItems
+ const symbols = assetProfileIdentifiers
.filter(({ symbol }) => {
return !isDerivedCurrency(getCurrencyFromSymbol(symbol));
})
@@ -634,13 +641,13 @@ export class DataProviderService {
}
private hasCurrency({
- currency,
- dataGatheringItems
+ assetProfileIdentifiers,
+ currency
}: {
+ assetProfileIdentifiers: AssetProfileIdentifier[];
currency: string;
- dataGatheringItems: AssetProfileIdentifier[];
}) {
- return dataGatheringItems.some(({ dataSource, symbol }) => {
+ return assetProfileIdentifiers.some(({ dataSource, symbol }) => {
return (
dataSource === this.getDataSourceForExchangeRates() &&
symbol === currency
diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
index dc8cc599..eedad747 100644
--- a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
+++ b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
@@ -89,7 +89,7 @@ export class DataGatheringProcessor {
);
const historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: currentDate,
to: new Date()
});
diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
index 72b8ac71..b79b2a09 100644
--- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
+++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts
@@ -122,7 +122,7 @@ export class DataGatheringService {
}) {
try {
const historicalData = await this.dataProviderService.getHistoricalRaw({
- dataGatheringItems: [{ dataSource, symbol }],
+ assetProfileIdentifiers: [{ dataSource, symbol }],
from: date,
to: date
});
From 8fb484af4d066b913343259ca579d82c72e562d6 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 7 Nov 2024 20:03:37 +0100
Subject: [PATCH 23/65] Bugfix/disable caching of benchmarks in markets
overview if sharing (#4027)
* Disable caching of benchmarks if sharing mode
* Update changelog
---
CHANGELOG.md | 1 +
apps/api/src/app/benchmark/benchmark.service.ts | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ae5de6f..2a94eaf1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Improved the exception handling in the user authorization service
+- Disabled the caching of the benchmarks in the markets overview if sharing the _Fear & Greed Index_ (market mood) is enabled
## 2.121.1 - 2024-11-02
diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts
index 36f19684..a659281d 100644
--- a/apps/api/src/app/benchmark/benchmark.service.ts
+++ b/apps/api/src/app/benchmark/benchmark.service.ts
@@ -437,7 +437,7 @@ export class BenchmarkService {
};
});
- if (storeInCache) {
+ if (!enableSharing && storeInCache) {
const expiration = addHours(new Date(), 2);
await this.redisCacheService.set(
From 70f2f01f8f78286c96d2abbbc019f21dd33c5b07 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 7 Nov 2024 20:05:01 +0100
Subject: [PATCH 24/65] Bugfix/fix algebraic sign in label of treemap chart
(#4030)
* Fix algebraic sign
* Update changelog
---
CHANGELOG.md | 1 +
.../lib/treemap-chart/treemap-chart.component.ts | 15 ++++++++++++---
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a94eaf1..97ceff3f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- Fixed an issue with the algebraic sign in the chart of the holdings tab on the home page (experimental)
- Improved the exception handling in the user authorization service
- Disabled the caching of the benchmarks in the markets overview if sharing the _Fear & Greed Index_ (market mood) is enabled
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 1acc2c92..9a8594ad 100644
--- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
+++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
@@ -261,12 +261,21 @@ export class GfTreemapChartComponent
display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: (ctx) => {
- const netPerformancePercentWithCurrencyEffect =
- ctx.raw._data.netPerformancePercentWithCurrencyEffect;
+ // Round to 4 decimal places
+ let netPerformancePercentWithCurrencyEffect =
+ Math.round(
+ ctx.raw._data.netPerformancePercentWithCurrencyEffect * 10000
+ ) / 10000;
+
+ if (Math.abs(netPerformancePercentWithCurrencyEffect) === 0) {
+ netPerformancePercentWithCurrencyEffect = Math.abs(
+ netPerformancePercentWithCurrencyEffect
+ );
+ }
return [
ctx.raw._data.symbol,
- `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(ctx.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`
+ `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`
];
},
hoverColor: undefined,
From f800650c4d4df511dbdfbb1bf3c03129ecadd9c9 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Thu, 7 Nov 2024 20:12:27 +0100
Subject: [PATCH 25/65] Release 2.122.0 (#4033)
---
CHANGELOG.md | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97ceff3f..16dad74a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## 2.122.0 - 2024-11-07
### Changed
diff --git a/package-lock.json b/package-lock.json
index 9dfb1f59..5edb87ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.121.1",
+ "version": "2.122.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.121.1",
+ "version": "2.122.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index 06bee046..8aa893ed 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.121.1",
+ "version": "2.122.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
From 9f72835d583dadaf4df626125198b0744e56ba26 Mon Sep 17 00:00:00 2001
From: Gianluca755 <19376177+Gianluca755@users.noreply.github.com>
Date: Sat, 9 Nov 2024 20:20:13 +0100
Subject: [PATCH 26/65] Feature/improve language localization for it 20241109
(#4036)
* Update translations
* Update changelog
---
CHANGELOG.md | 1 +
apps/client/src/locales/messages.it.xlf | 108 ++++++++++++------------
2 files changed, 55 insertions(+), 54 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16dad74a..8131113f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Improved the language localization for Italian (`it`)
- Upgraded `countries-list` from version `3.1.0` to `3.1.1`
### Fixed
diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf
index 9364d835..8f55b327 100644
--- a/apps/client/src/locales/messages.it.xlf
+++ b/apps/client/src/locales/messages.it.xlf
@@ -2872,7 +2872,7 @@
Hello, has shared a Portfolio with you!
- Salve, ha condiviso un Portafoglio con te!
+ Salve, ha condiviso un Portafoglio con te!
apps/client/src/app/pages/public/public-page.html
4
@@ -5881,7 +5881,7 @@
Currency Cluster Risks
- Currency Cluster Risks
+ Rischio di Concentrazione Valutario
apps/client/src/app/pages/portfolio/fire/fire-page.html
141
@@ -5889,7 +5889,7 @@
Account Cluster Risks
- Account Cluster Risks
+ Rischi di Concentrazione dei Conti
apps/client/src/app/pages/portfolio/fire/fire-page.html
160
@@ -6237,7 +6237,7 @@
Restricted view
- Restricted view
+ Vista limitata
apps/client/src/app/components/access-table/access-table.component.html
26
@@ -6357,7 +6357,7 @@
WTD
- WTD
+ Settimana corrente
libs/ui/src/lib/assistant/assistant.component.ts
212
@@ -6373,7 +6373,7 @@
MTD
- MTD
+ Mese corrente
libs/ui/src/lib/assistant/assistant.component.ts
216
@@ -6597,7 +6597,7 @@
{VAR_PLURAL, plural, =1 {activity} other {activities}}
- {VAR_PLURAL, plural, =1 {activity} other {activities}}
+ {VAR_PLURAL, plural, =1 {attività} other {attività}}
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
14
@@ -6781,7 +6781,7 @@
Family Office
- Family Office
+ Ufficio familiare
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
87
@@ -6837,7 +6837,7 @@
User Experience
- User Experience
+ Esperienza Utente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
98
@@ -7061,7 +7061,7 @@
Copy link to clipboard
- Copy link to clipboard
+ Copia link negli appunti
apps/client/src/app/components/access-table/access-table.component.html
70
@@ -7069,7 +7069,7 @@
Portfolio Snapshot
- Portfolio Snapshot
+ Stato del Portfolio
apps/client/src/app/components/admin-jobs/admin-jobs.html
39
@@ -7077,7 +7077,7 @@
Change with currency effect Change
- Change with currency effect Change
+ Cambio con effetto valuta Cambia
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
50
@@ -7085,7 +7085,7 @@
Performance with currency effect Performance
- Performance with currency effect Performance
+ Prestazioni con effetto valuta Prestazioni
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
69
@@ -7093,7 +7093,7 @@
Threshold Min
- Threshold Min
+ Soglia Minima
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
9
@@ -7101,7 +7101,7 @@
Threshold Max
- Threshold Max
+ Soglia Massima
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
44
@@ -7109,7 +7109,7 @@
Close
- Close
+ Chiudi
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
77
@@ -7117,7 +7117,7 @@
Customize
- Customize
+ Personalizza
apps/client/src/app/components/rule/rule.component.html
67
@@ -7125,7 +7125,7 @@
No auto-renewal.
- No auto-renewal.
+ No rinnovo automatico.
apps/client/src/app/components/user-account-membership/user-account-membership.html
62
@@ -7133,7 +7133,7 @@
Today
- Today
+ Oggi
apps/client/src/app/pages/public/public-page.html
24
@@ -7141,7 +7141,7 @@
This year
- This year
+ Anno corrente
apps/client/src/app/pages/public/public-page.html
42
@@ -7149,7 +7149,7 @@
From the beginning
- From the beginning
+ Dall'inizio
apps/client/src/app/pages/public/public-page.html
60
@@ -7157,7 +7157,7 @@
Oops! Invalid currency.
- Oops! Invalid currency.
+ Oops! Valuta sbagliata.
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html
49
@@ -7165,7 +7165,7 @@
This page has been archived.
- This page has been archived.
+ Questa pagina è stata archiviata.
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
14
@@ -7173,7 +7173,7 @@
is Open Source Software
- is Open Source Software
+ è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
139
@@ -7181,7 +7181,7 @@
is not Open Source Software
- is not Open Source Software
+ non è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
146
@@ -7189,7 +7189,7 @@
is Open Source Software
- is Open Source Software
+ è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
156
@@ -7197,7 +7197,7 @@
is not Open Source Software
- is not Open Source Software
+ non è un programma Open Source
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
163
@@ -7205,7 +7205,7 @@
can be self-hosted
- can be self-hosted
+ può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
178
@@ -7213,7 +7213,7 @@
cannot be self-hosted
- cannot be self-hosted
+ non può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
185
@@ -7221,7 +7221,7 @@
can be self-hosted
- can be self-hosted
+ può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
195
@@ -7229,7 +7229,7 @@
cannot be self-hosted
- cannot be self-hosted
+ non può essere ospitato in proprio
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
202
@@ -7237,7 +7237,7 @@
can be used anonymously
- can be used anonymously
+ può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
217
@@ -7245,7 +7245,7 @@
cannot be used anonymously
- cannot be used anonymously
+ non può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
224
@@ -7253,7 +7253,7 @@
can be used anonymously
- can be used anonymously
+ può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
234
@@ -7261,7 +7261,7 @@
cannot be used anonymously
- cannot be used anonymously
+ non può essere usato anonimamente
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
241
@@ -7269,7 +7269,7 @@
offers a free plan
- offers a free plan
+ ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
256
@@ -7277,7 +7277,7 @@
does not offer a free plan
- does not offer a free plan
+ non ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
263
@@ -7285,7 +7285,7 @@
offers a free plan
- offers a free plan
+ ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
273
@@ -7293,7 +7293,7 @@
does not offer a free plan
- does not offer a free plan
+ non ha un piano gratuito
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
280
@@ -7301,7 +7301,7 @@
Oops! Could not find any assets.
- Oops! Could not find any assets.
+ Oops! Non ho trovato alcun asset.
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
37
@@ -7309,7 +7309,7 @@
Data Providers
- Data Providers
+ Fornitori di dati
apps/client/src/app/components/admin-settings/admin-settings.component.html
4
@@ -7317,7 +7317,7 @@
NEW
- NEW
+ NUOVO
apps/client/src/app/components/admin-settings/admin-settings.component.html
14
@@ -7325,7 +7325,7 @@
Set API Key
- Set API Key
+ Imposta API Key
apps/client/src/app/components/admin-settings/admin-settings.component.html
29
@@ -7333,7 +7333,7 @@
Want to stay updated? Click below to get notified as soon as it’s available.
- Want to stay updated? Click below to get notified as soon as it’s available.
+ Vuoi seguire le novità? Clicca sotto per essere notificato appena è disponibile.
apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
23
@@ -7341,7 +7341,7 @@
Notify me
- Notify me
+ Notificami
apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html
32
@@ -7349,7 +7349,7 @@
Get access to 100’000+ tickers from over 50 exchanges
- Get access to 100’000+ tickers from over 50 exchanges
+ Ottieni accesso a oltre 100’000+ titoli da oltre 50 borse
libs/ui/src/lib/i18n.ts
24
@@ -7357,7 +7357,7 @@
Ukraine
- Ukraine
+ Ucraina
libs/ui/src/lib/i18n.ts
92
@@ -7365,7 +7365,7 @@
Skip
- Skip
+ Salta
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
83
@@ -7373,7 +7373,7 @@
Join now
- Join now
+ Iscriviti adesso
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
93
@@ -7381,7 +7381,7 @@
Allocation Cluster Risks
- Allocation Cluster Risks
+ Rischi di allocazione dei Conti
apps/client/src/app/pages/portfolio/fire/fire-page.html
179
@@ -7389,7 +7389,7 @@
Glossary
- Glossary
+ Glossario
apps/client/src/app/pages/resources/glossary/resources-glossary-routing.module.ts
10
@@ -7401,7 +7401,7 @@
Guides
- Guides
+ Guide
apps/client/src/app/pages/resources/guides/resources-guides-routing.module.ts
10
@@ -7413,7 +7413,7 @@
guides
- guides
+ guide
snake-case
apps/client/src/app/pages/resources/overview/resources-overview.component.ts
@@ -7426,7 +7426,7 @@
glossary
- glossary
+ glossario
snake-case
apps/client/src/app/pages/resources/overview/resources-overview.component.ts
From 6057794eb6649f0542e9261418df5e478e5b5109 Mon Sep 17 00:00:00 2001
From: Amandee Ellawala <47607256+amandee27@users.noreply.github.com>
Date: Sun, 10 Nov 2024 09:29:43 +0000
Subject: [PATCH 27/65] Feature/extend assistant by holding selector (#4031)
* Extend assistant by holding selector
* Update changelog
---
CHANGELOG.md | 6 ++
.../src/app/portfolio/portfolio.controller.ts | 25 +++++
.../src/app/user/update-user-setting.dto.ts | 8 ++
.../app/components/header/header.component.ts | 14 +--
apps/client/src/app/services/data.service.ts | 2 +-
.../src/app/services/user/user.service.ts | 14 +++
.../lib/interfaces/user-settings.interface.ts | 2 +
.../src/lib/assistant/assistant.component.ts | 94 ++++++++++++++++---
libs/ui/src/lib/assistant/assistant.html | 28 ++++++
9 files changed, 172 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8131113f..9a9aacf3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Changed
+
+- Extended the assistant by a holding selector
+
## 2.122.0 - 2024-11-07
### Changed
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 326dda15..f2415dff 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -74,12 +74,15 @@ export class PortfolioController {
@Get('details')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withMarkets') withMarketsParam = 'false'
): Promise {
@@ -95,6 +98,8 @@ export class PortfolioController {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -289,17 +294,22 @@ export class PortfolioController {
@Get('dividends')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getDividends(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -356,21 +366,26 @@ export class PortfolioController {
@Get('holdings')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getHoldings(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('holdingType') filterByHoldingType?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
filterByHoldingType,
filterBySearchQuery,
+ filterBySymbol,
filterByTags
});
@@ -386,17 +401,22 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getInvestments(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
): Promise {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
@@ -451,13 +471,16 @@ export class PortfolioController {
@Get('performance')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(PerformanceLoggingInterceptor)
+ @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = 'max',
+ @Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
): Promise {
@@ -466,6 +489,8 @@ export class PortfolioController {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
+ filterByDataSource,
+ filterBySymbol,
filterByTags
});
diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts
index 13a3a5d2..b34b6fae 100644
--- a/apps/api/src/app/user/update-user-setting.dto.ts
+++ b/apps/api/src/app/user/update-user-setting.dto.ts
@@ -64,6 +64,14 @@ export class UpdateUserSettingDto {
@IsOptional()
'filters.assetClasses'?: string[];
+ @IsString()
+ @IsOptional()
+ 'filters.dataSource'?: string;
+
+ @IsString()
+ @IsOptional()
+ 'filters.symbol'?: string;
+
@IsArray()
@IsOptional()
'filters.tags'?: string[];
diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts
index 1739d113..004fa5f3 100644
--- a/apps/client/src/app/components/header/header.component.ts
+++ b/apps/client/src/app/components/header/header.component.ts
@@ -175,17 +175,17 @@ export class HeaderComponent implements OnChanges {
const userSetting: UpdateUserSettingDto = {};
for (const filter of filters) {
- let filtersType: string;
-
if (filter.type === 'ACCOUNT') {
- filtersType = 'accounts';
+ userSetting['filters.accounts'] = filter.id ? [filter.id] : null;
} else if (filter.type === 'ASSET_CLASS') {
- filtersType = 'assetClasses';
+ userSetting['filters.assetClasses'] = filter.id ? [filter.id] : null;
+ } else if (filter.type === 'DATA_SOURCE') {
+ userSetting['filters.dataSource'] = filter.id ? filter.id : null;
+ } else if (filter.type === 'SYMBOL') {
+ userSetting['filters.symbol'] = filter.id ? filter.id : null;
} else if (filter.type === 'TAG') {
- filtersType = 'tags';
+ userSetting['filters.tags'] = filter.id ? [filter.id] : null;
}
-
- userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;
}
this.dataService
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index cde7555b..dccbb064 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -532,7 +532,7 @@ export class DataService {
}: {
filters?: Filter[];
range?: DateRange;
- }) {
+ } = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (range) {
diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts
index 3ecc58c1..aa91a90b 100644
--- a/apps/client/src/app/services/user/user.service.ts
+++ b/apps/client/src/app/services/user/user.service.ts
@@ -65,6 +65,20 @@ export class UserService extends ObservableStore {
});
}
+ if (user?.settings['filters.dataSource']) {
+ filters.push({
+ id: user.settings['filters.dataSource'],
+ type: 'DATA_SOURCE'
+ });
+ }
+
+ if (user?.settings['filters.symbol']) {
+ filters.push({
+ id: user.settings['filters.symbol'],
+ type: 'SYMBOL'
+ });
+ }
+
if (user?.settings['filters.tags']) {
filters.push({
id: user.settings['filters.tags'][0],
diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts
index e9e90e71..d72be7c7 100644
--- a/libs/common/src/lib/interfaces/user-settings.interface.ts
+++ b/libs/common/src/lib/interfaces/user-settings.interface.ts
@@ -14,6 +14,8 @@ export interface UserSettings {
dateRange?: DateRange;
emergencyFund?: number;
'filters.accounts'?: string[];
+ 'filters.dataSource'?: string;
+ 'filters.symbol'?: string;
'filters.tags'?: string[];
holdingsViewMode?: HoldingsViewMode;
isExperimentalFeatures?: boolean;
diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts
index d73cdb41..3a5e6a2f 100644
--- a/libs/ui/src/lib/assistant/assistant.component.ts
+++ b/libs/ui/src/lib/assistant/assistant.component.ts
@@ -1,7 +1,9 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
+import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
-import { Filter, User } from '@ghostfolio/common/interfaces';
+import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
+import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
@@ -35,7 +37,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
-import { Account, AssetClass } from '@prisma/client';
+import { Account, AssetClass, DataSource } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import {
@@ -61,6 +63,7 @@ import {
FormsModule,
GfAssetProfileIconComponent,
GfAssistantListItemComponent,
+ GfSymbolModule,
MatButtonModule,
MatFormFieldModule,
MatSelectModule,
@@ -132,8 +135,10 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
public filterForm = this.formBuilder.group({
account: new FormControl(undefined),
assetClass: new FormControl(undefined),
+ holding: new FormControl(undefined),
tag: new FormControl(undefined)
});
+ public holdings: PortfolioPosition[] = [];
public isLoading = false;
public isOpen = false;
public placeholder = $localize`Find holding...`;
@@ -144,7 +149,13 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
};
public tags: Filter[] = [];
- private filterTypes: Filter['type'][] = ['ACCOUNT', 'ASSET_CLASS', 'TAG'];
+ private filterTypes: Filter['type'][] = [
+ 'ACCOUNT',
+ 'ASSET_CLASS',
+ 'DATA_SOURCE',
+ 'SYMBOL',
+ 'TAG'
+ ];
private keyManager: FocusKeyManager;
private unsubscribeSubject = new Subject();
@@ -156,6 +167,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
) {}
public ngOnInit() {
+ this.initializeFilterForm();
+
this.assetClasses = Object.keys(AssetClass).map((assetClass) => {
return {
id: assetClass,
@@ -263,16 +276,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.filterForm.enable({ emitEvent: false });
}
- this.filterForm.setValue(
- {
- account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
- assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
- tag: this.user?.settings?.['filters.tags']?.[0] ?? null
- },
- {
- emitEvent: false
- }
- );
+ this.initializeFilterForm();
this.tags =
this.user?.tags
@@ -298,6 +302,19 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
});
}
+ public holdingComparisonFunction(
+ option: PortfolioPosition,
+ value: PortfolioPosition
+ ): boolean {
+ if (value === null) {
+ return false;
+ }
+
+ return (
+ getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value)
+ );
+ }
+
public async initialize() {
this.isLoading = true;
this.keyManager = new FocusKeyManager(this.assistantListItems).withWrap();
@@ -331,6 +348,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
id: this.filterForm.get('assetClass').value,
type: 'ASSET_CLASS'
},
+ {
+ id: this.filterForm.get('holding').value?.dataSource,
+ type: 'DATA_SOURCE'
+ },
+ {
+ id: this.filterForm.get('holding').value?.symbol,
+ type: 'SYMBOL'
+ },
{
id: this.filterForm.get('tag').value,
type: 'TAG'
@@ -473,4 +498,47 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject)
);
}
+
+ private initializeFilterForm() {
+ this.dataService
+ .fetchPortfolioHoldings()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(({ holdings }) => {
+ this.holdings = holdings
+ .filter(({ assetSubClass }) => {
+ return !['CASH'].includes(assetSubClass);
+ })
+ .sort((a, b) => {
+ return a.name?.localeCompare(b.name);
+ });
+ this.setFilterFormValues();
+ });
+ }
+
+ private setFilterFormValues() {
+ const dataSource = this.user?.settings?.[
+ 'filters.dataSource'
+ ] as DataSource;
+ const symbol = this.user?.settings?.['filters.symbol'];
+ const selectedHolding = this.holdings.find((holding) => {
+ return (
+ getAssetProfileIdentifier({
+ dataSource: holding.dataSource,
+ symbol: holding.symbol
+ }) === getAssetProfileIdentifier({ dataSource, symbol })
+ );
+ });
+
+ this.filterForm.setValue(
+ {
+ account: this.user?.settings?.['filters.accounts']?.[0] ?? null,
+ assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null,
+ holding: selectedHolding ?? null,
+ tag: this.user?.settings?.['filters.tags']?.[0] ?? null
+ },
+ {
+ emitEvent: false
+ }
+ );
+ }
}
diff --git a/libs/ui/src/lib/assistant/assistant.html b/libs/ui/src/lib/assistant/assistant.html
index 648c791a..18c2145a 100644
--- a/libs/ui/src/lib/assistant/assistant.html
+++ b/libs/ui/src/lib/assistant/assistant.html
@@ -122,6 +122,34 @@
+
+
+ Holding
+
+ {{
+ filterForm.get('holding')?.value?.name
+ }}
+
+ @for (holding of holdings; track holding.name) {
+
+
+ {{ holding.name }}
+
+ {{ holding.symbol | gfSymbol }} ·
+ {{ holding.currency }}
+
+
+ }
+
+
+
Tags
From 09a9148fecf8067e3bada5201a45f86900fe3b77 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 10 Nov 2024 11:01:39 +0100
Subject: [PATCH 28/65] Bugfix/move changelog entry (#4038)
* Move changelog entry
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a9aacf3..5bb45d74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the assistant by a holding selector
+- Improved the language localization for Italian (`it`)
## 2.122.0 - 2024-11-07
### Changed
-- Improved the language localization for Italian (`it`)
- Upgraded `countries-list` from version `3.1.0` to `3.1.1`
### Fixed
From 15856264f8522fb4680cdd36d3bbbbb7fec00066 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 11 Nov 2024 19:26:43 +0100
Subject: [PATCH 29/65] Feature/upgrade ngx-skeleton-loader to version 9.0.0
(#4026)
* Upgrade ngx-skeleton-loader to version 9.0.0
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 25 ++++++-------------------
package.json | 2 +-
3 files changed, 8 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bb45d74..2700af21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extended the assistant by a holding selector
- Improved the language localization for Italian (`it`)
+- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
## 2.122.0 - 2024-11-07
diff --git a/package-lock.json b/package-lock.json
index 5edb87ea..fff9ff65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -77,7 +77,7 @@
"ng-extract-i18n-merge": "2.12.0",
"ngx-device-detector": "8.0.0",
"ngx-markdown": "18.0.0",
- "ngx-skeleton-loader": "7.0.0",
+ "ngx-skeleton-loader": "9.0.0",
"ngx-stripe": "18.1.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
@@ -26858,17 +26858,16 @@
}
},
"node_modules/ngx-skeleton-loader": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-7.0.0.tgz",
- "integrity": "sha512-myc6GNcNhyksZrimIFkCxeihi0kQ8JhQVZiGbtiIv4gYrnnRk5nXbs3kYitK8E8OstHG+jlsmRofqGBxuIsYTA==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-9.0.0.tgz",
+ "integrity": "sha512-aO4/V6oGdZGNcTjasTg/fwzJJYl/ZmNKgCukOEQdUK3GSFOZtB/3GGULMJuZ939hk3Hzqh1OBiLfIM1SqTfhqg==",
"license": "MIT",
"dependencies": {
- "perf-marks": "^1.13.4",
"tslib": "^2.0.0"
},
"peerDependencies": {
- "@angular/common": ">=8.0.0",
- "@angular/core": ">=8.0.0"
+ "@angular/common": ">=16.0.0",
+ "@angular/core": ">=16.0.0"
}
},
"node_modules/ngx-stripe": {
@@ -28423,18 +28422,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/perf-marks": {
- "version": "1.14.2",
- "resolved": "https://registry.npmjs.org/perf-marks/-/perf-marks-1.14.2.tgz",
- "integrity": "sha512-N0/bQcuTlETpgox/DsXS1voGjqaoamMoiyhncgeW3rSHy/qw8URVgmPRYfFDQns/+C6yFUHDbeSBGL7ixT6Y4A==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.1.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
diff --git a/package.json b/package.json
index 8aa893ed..ca24d28b 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
"ng-extract-i18n-merge": "2.12.0",
"ngx-device-detector": "8.0.0",
"ngx-markdown": "18.0.0",
- "ngx-skeleton-loader": "7.0.0",
+ "ngx-skeleton-loader": "9.0.0",
"ngx-stripe": "18.1.0",
"open-color": "1.9.1",
"papaparse": "5.3.1",
From 92b025bff3a7f770d920cb58fbe5bbc662ee5ac2 Mon Sep 17 00:00:00 2001
From: Mohan <41165473+mohanbyte@users.noreply.github.com>
Date: Tue, 12 Nov 2024 01:37:02 +0530
Subject: [PATCH 30/65] Feature/separate FIRE and X-ray pages (#4037)
* Separate FIRE / X-ray page
* Update changelog
---
CHANGELOG.md | 1 +
.../fire/fire-page-routing.module.ts | 2 +-
.../portfolio/fire/fire-page.component.ts | 93 +----------
.../app/pages/portfolio/fire/fire-page.html | 130 ---------------
.../pages/portfolio/fire/fire-page.module.ts | 2 -
.../portfolio-page-routing.module.ts | 5 +
.../portfolio/portfolio-page.component.ts | 7 +-
.../x-ray/x-ray-page-routing.module.ts | 21 +++
.../portfolio/x-ray/x-ray-page.component.html | 123 ++++++++++++++
.../portfolio/x-ray/x-ray-page.component.scss | 3 +
.../portfolio/x-ray/x-ray-page.component.ts | 150 ++++++++++++++++++
.../portfolio/x-ray/x-ray-page.module.ts | 22 +++
12 files changed, 333 insertions(+), 226 deletions(-)
create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts
create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss
create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
create mode 100644 apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2700af21..8f3628ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the assistant by a holding selector
+- Separated the _FIRE_ / _X-ray_ page
- Improved the language localization for Italian (`it`)
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
index 885dc550..96260add 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page-routing.module.ts
@@ -10,7 +10,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FirePageComponent,
path: '',
- title: $localize`FIRE`
+ title: 'FIRE'
}
];
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
index d20c6691..897b9824 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
@@ -1,12 +1,7 @@
-import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import {
- PortfolioReport,
- PortfolioReportRule,
- User
-} from '@ghostfolio/common/interfaces';
+import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@@ -21,18 +16,11 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './fire-page.html'
})
export class FirePageComponent implements OnDestroy, OnInit {
- public accountClusterRiskRules: PortfolioReportRule[];
- public currencyClusterRiskRules: PortfolioReportRule[];
public deviceType: string;
- public economicMarketClusterRiskRules: PortfolioReportRule[];
- public emergencyFundRules: PortfolioReportRule[];
- public feeRules: PortfolioReportRule[];
public fireWealth: Big;
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
- public inactiveRules: PortfolioReportRule[];
public isLoading = false;
- public isLoadingPortfolioReport = false;
public user: User;
public withdrawalRatePerMonth: Big;
public withdrawalRatePerYear: Big;
@@ -95,8 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
}
});
-
- this.initializePortfolioReport();
}
public onAnnualInterestRateChange(annualInterestRate: number) {
@@ -133,21 +119,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
});
});
}
-
- public onRulesUpdated(event: UpdateUserSettingDto) {
- this.dataService
- .putUserSetting(event)
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe(() => {
- this.userService
- .get(true)
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe();
-
- this.initializePortfolioReport();
- });
- }
-
public onSavingsRateChange(savingsRate: number) {
this.dataService
.putUserSetting({ savingsRate })
@@ -187,66 +158,4 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
-
- private initializePortfolioReport() {
- this.isLoadingPortfolioReport = true;
-
- this.dataService
- .fetchPortfolioReport()
- .pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((portfolioReport) => {
- this.inactiveRules = this.mergeInactiveRules(portfolioReport);
-
- this.accountClusterRiskRules =
- portfolioReport.rules['accountClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.currencyClusterRiskRules =
- portfolioReport.rules['currencyClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.economicMarketClusterRiskRules =
- portfolioReport.rules['economicMarketClusterRisk']?.filter(
- ({ isActive }) => {
- return isActive;
- }
- ) ?? null;
-
- this.emergencyFundRules =
- portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
- return isActive;
- }) ?? null;
-
- this.feeRules =
- portfolioReport.rules['fees']?.filter(({ isActive }) => {
- return isActive;
- }) ?? null;
-
- this.isLoadingPortfolioReport = false;
-
- this.changeDetectorRef.markForCheck();
- });
- }
-
- private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
- let inactiveRules: PortfolioReportRule[] = [];
-
- for (const category in report.rules) {
- const rulesArray = report.rules[category];
-
- inactiveRules = inactiveRules.concat(
- rulesArray.filter(({ isActive }) => {
- return !isActive;
- })
- );
- }
-
- return inactiveRules;
- }
}
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 7a336b62..77fd1640 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.html
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html
@@ -101,133 +101,3 @@
}
-
-
-
-
-
X-ray
-
- Ghostfolio X-ray uses static analysis to identify potential issues
- and risks in your portfolio.
- It will be highly configurable in the future: activate / deactivate
- rules and customize the thresholds to match your personal investment
- style.
-
-
-
- Emergency Fund
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Currency Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Account Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Economic Market Cluster Risks
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
- Fees
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
- @if (inactiveRules?.length > 0) {
-
-
Inactive
-
-
- }
-
-
-
diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
index 60e3127d..a606ae1b 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.module.ts
@@ -1,4 +1,3 @@
-import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
@@ -17,7 +16,6 @@ import { FirePageComponent } from './fire-page.component';
FirePageRoutingModule,
GfFireCalculatorComponent,
GfPremiumIndicatorComponent,
- GfRulesModule,
GfValueComponent,
NgxSkeletonLoaderModule
],
diff --git a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
index 6146c573..20de6f8f 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
+++ b/apps/client/src/app/pages/portfolio/portfolio-page-routing.module.ts
@@ -34,6 +34,11 @@ const routes: Routes = [
path: 'fire',
loadChildren: () =>
import('./fire/fire-page.module').then((m) => m.FirePageModule)
+ },
+ {
+ path: 'x-ray',
+ loadChildren: () =>
+ import('./x-ray/x-ray-page.module').then((m) => m.XRayPageModule)
}
],
component: PortfolioPageComponent,
diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
index 0c980e25..7f40bf1d 100644
--- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts
@@ -46,8 +46,13 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
},
{
iconName: 'calculator-outline',
- label: 'FIRE / X-ray',
+ label: 'FIRE ',
path: ['/portfolio', 'fire']
+ },
+ {
+ iconName: 'scan-outline',
+ label: 'X-ray',
+ path: ['/portfolio', 'x-ray']
}
];
this.user = state.user;
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts
new file mode 100644
index 00000000..091cbc49
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page-routing.module.ts
@@ -0,0 +1,21 @@
+import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { XRayPageComponent } from './x-ray-page.component';
+
+const routes: Routes = [
+ {
+ canActivate: [AuthGuard],
+ component: XRayPageComponent,
+ path: '',
+ title: 'X-ray'
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class XRayPageRoutingModule {}
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
new file mode 100644
index 00000000..cd03b49b
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
@@ -0,0 +1,123 @@
+
+
+
+
X-ray
+
+ Ghostfolio X-ray uses static analysis to uncover potential issues and
+ risks in your portfolio. Adjust the rules below and set custom
+ thresholds to align with your personal investment strategy.
+
+
+
+ Emergency Fund
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Currency Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Account Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Economic Market Cluster Risks
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+ Fees
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+ @if (inactiveRules?.length > 0) {
+
+
Inactive
+
+
+ }
+
+
+
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss
new file mode 100644
index 00000000..5d4e87f3
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.scss
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
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
new file mode 100644
index 00000000..36f42fc3
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
@@ -0,0 +1,150 @@
+import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
+import { DataService } from '@ghostfolio/client/services/data.service';
+import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
+import { UserService } from '@ghostfolio/client/services/user/user.service';
+import {
+ PortfolioReportRule,
+ PortfolioReport
+} from '@ghostfolio/common/interfaces';
+import { User } from '@ghostfolio/common/interfaces/user.interface';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+
+import { ChangeDetectorRef, Component } from '@angular/core';
+import { Subject, takeUntil } from 'rxjs';
+
+@Component({
+ selector: 'gf-x-ray-page',
+ styleUrl: './x-ray-page.component.scss',
+ templateUrl: './x-ray-page.component.html'
+})
+export class XRayPageComponent {
+ public accountClusterRiskRules: PortfolioReportRule[];
+ public currencyClusterRiskRules: PortfolioReportRule[];
+ public economicMarketClusterRiskRules: PortfolioReportRule[];
+ public emergencyFundRules: PortfolioReportRule[];
+ public feeRules: PortfolioReportRule[];
+ public hasImpersonationId: boolean;
+ public hasPermissionToUpdateUserSettings: boolean;
+ public inactiveRules: PortfolioReportRule[];
+ public isLoadingPortfolioReport = false;
+ public user: User;
+
+ private unsubscribeSubject = new Subject();
+
+ public constructor(
+ private changeDetectorRef: ChangeDetectorRef,
+ private dataService: DataService,
+ private impersonationStorageService: ImpersonationStorageService,
+ private userService: UserService
+ ) {}
+
+ public ngOnInit() {
+ this.impersonationStorageService
+ .onChangeHasImpersonation()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((impersonationId) => {
+ this.hasImpersonationId = !!impersonationId;
+ });
+
+ this.userService.stateChanged
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((state) => {
+ if (state?.user) {
+ this.user = state.user;
+
+ this.hasPermissionToUpdateUserSettings =
+ this.user.subscription?.type === 'Basic'
+ ? false
+ : hasPermission(
+ this.user.permissions,
+ permissions.updateUserSettings
+ );
+
+ this.changeDetectorRef.markForCheck();
+ }
+ });
+
+ this.initializePortfolioReport();
+ }
+
+ public onRulesUpdated(event: UpdateUserSettingDto) {
+ this.dataService
+ .putUserSetting(event)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe();
+
+ this.initializePortfolioReport();
+ });
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+
+ private initializePortfolioReport() {
+ this.isLoadingPortfolioReport = true;
+
+ this.dataService
+ .fetchPortfolioReport()
+ .pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((portfolioReport) => {
+ this.inactiveRules = this.mergeInactiveRules(portfolioReport);
+
+ this.accountClusterRiskRules =
+ portfolioReport.rules['accountClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.currencyClusterRiskRules =
+ portfolioReport.rules['currencyClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.economicMarketClusterRiskRules =
+ portfolioReport.rules['economicMarketClusterRisk']?.filter(
+ ({ isActive }) => {
+ return isActive;
+ }
+ ) ?? null;
+
+ this.emergencyFundRules =
+ portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
+
+ this.feeRules =
+ portfolioReport.rules['fees']?.filter(({ isActive }) => {
+ return isActive;
+ }) ?? null;
+
+ this.isLoadingPortfolioReport = false;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
+ private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
+ let inactiveRules: PortfolioReportRule[] = [];
+
+ for (const category in report.rules) {
+ const rulesArray = report.rules[category];
+
+ inactiveRules = inactiveRules.concat(
+ rulesArray.filter(({ isActive }) => {
+ return !isActive;
+ })
+ );
+ }
+
+ return inactiveRules;
+ }
+}
diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts
new file mode 100644
index 00000000..bff4f4dc
--- /dev/null
+++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.module.ts
@@ -0,0 +1,22 @@
+import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
+import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
+
+import { CommonModule } from '@angular/common';
+import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+
+import { XRayPageRoutingModule } from './x-ray-page-routing.module';
+import { XRayPageComponent } from './x-ray-page.component';
+
+@NgModule({
+ declarations: [XRayPageComponent],
+ imports: [
+ CommonModule,
+ GfPremiumIndicatorComponent,
+ GfRulesModule,
+ NgxSkeletonLoaderModule,
+ XRayPageRoutingModule
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+})
+export class XRayPageModule {}
From 95cb6dcb8d8289facb8ef70e3b851b8cb64f1811 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Wed, 13 Nov 2024 14:34:30 +0100
Subject: [PATCH 31/65] Feature/upgrade UUID to version 11.0.2 (#4029)
* Upgrade uuid to version 11.0.2
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 38 +++++++++++++++++++++++++++++++++-----
package.json | 2 +-
3 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f3628ca..44a09a6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Separated the _FIRE_ / _X-ray_ page
- Improved the language localization for Italian (`it`)
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
+- Upgraded `uuid` from version `9.0.1` to `11.0.2`
## 2.122.0 - 2024-11-07
diff --git a/package-lock.json b/package-lock.json
index fff9ff65..f6351537 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -89,7 +89,7 @@
"stripe": "17.3.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
- "uuid": "9.0.1",
+ "uuid": "11.0.2",
"yahoo-finance2": "2.11.3",
"zone.js": "0.14.10"
},
@@ -8900,6 +8900,20 @@
"storybook": "^8.3.6"
}
},
+ "node_modules/@storybook/addon-actions/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@storybook/addon-backgrounds": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.3.6.tgz",
@@ -25771,6 +25785,20 @@
"web-worker": "^1.2.0"
}
},
+ "node_modules/mermaid/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -33552,16 +33580,16 @@
}
},
"node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
+ "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist/esm/bin/uuid"
}
},
"node_modules/uvu": {
diff --git a/package.json b/package.json
index ca24d28b..9fa4d045 100644
--- a/package.json
+++ b/package.json
@@ -135,7 +135,7 @@
"stripe": "17.3.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
- "uuid": "9.0.1",
+ "uuid": "11.0.2",
"yahoo-finance2": "2.11.3",
"zone.js": "0.14.10"
},
From 8c0de59414fe43eb4b65f9941f0cb3872963efcd Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 16 Nov 2024 00:02:28 +0100
Subject: [PATCH 32/65] Feature/move treemap chart from experimental to general
availability (#4034)
* Move treemap chart to general availability
* Update changelog
---
CHANGELOG.md | 1 +
.../home-holdings/home-holdings.html | 30 +++++++++----------
2 files changed, 15 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44a09a6b..db6ab362 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Moved the chart of the holdings tab on the home page from experimental to general availability
- Extended the assistant by a holding selector
- Separated the _FIRE_ / _X-ray_ page
- Improved the language localization for Italian (`it`)
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 f1c4e7e8..abbc93b3 100644
--- a/apps/client/src/app/components/home-holdings/home-holdings.html
+++ b/apps/client/src/app/components/home-holdings/home-holdings.html
@@ -7,23 +7,21 @@
- @if (user?.settings?.isExperimentalFeatures) {
-
-
-
-
-
-
-
-
-
-
-
+
Date: Sat, 16 Nov 2024 11:17:58 +0000
Subject: [PATCH 33/65] Feature/implement range slider in rule settings dialog
(#4043)
* Implement range slider in rule settings dialog
* Update changelog
---
CHANGELOG.md | 1 +
.../rule-settings-dialog.html | 188 +++++++++++-------
.../rule-settings-dialog.scss | 3 +
3 files changed, 124 insertions(+), 68 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db6ab362..89857f9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Moved the chart of the holdings tab on the home page from experimental to general availability
- Extended the assistant by a holding selector
- Separated the _FIRE_ / _X-ray_ page
+- Improved the usability to customize the rule thresholds in the _X-ray_ page by introducing range sliders (experimental)
- Improved the language localization for Italian (`it`)
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
- Upgraded `uuid` from version `9.0.1` to `11.0.2`
diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
index 8806dae6..97854ad7 100644
--- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
+++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
@@ -1,76 +1,128 @@
{{ data.rule.name }}
-
+ } @else {
+
-
-
- @if (data.rule.configuration.threshold.unit === '%') {
- {{
- data.rule.configuration.threshold.max | percent: '1.2-2'
- }}
- } @else {
- {{ data.rule.configuration.threshold.max }}
- }
-
-
-
- Threshold Max :
- @if (data.rule.configuration.threshold.unit === '%') {
- {{ data.settings.thresholdMax | percent: '1.2-2' }}
- } @else {
- {{ data.settings.thresholdMax }}
- }
-
- @if (data.rule.configuration.threshold.unit === '%') {
-
{{
- data.rule.configuration.threshold.min | percent: '1.2-2'
- }}
- } @else {
-
{{ data.rule.configuration.threshold.min }}
- }
-
+ Threshold Min :
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{ data.settings.thresholdMin | percent: '1.2-2' }}
+ } @else {
+ {{ data.settings.thresholdMin }}
+ }
+
+
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{
+ data.rule.configuration.threshold.min | percent: '1.2-2'
+ }}
+ } @else {
+ {{ data.rule.configuration.threshold.min }}
+ }
+
+
+
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{
+ data.rule.configuration.threshold.max | percent: '1.2-2'
+ }}
+ } @else {
+ {{ data.rule.configuration.threshold.max }}
+ }
+
+
+
-
-
- @if (data.rule.configuration.threshold.unit === '%') {
- {{
- data.rule.configuration.threshold.max | percent: '1.2-2'
- }}
- } @else {
- {{ data.rule.configuration.threshold.max }}
- }
-
+
+ Threshold Max :
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{ data.settings.thresholdMax | percent: '1.2-2' }}
+ } @else {
+ {{ data.settings.thresholdMax }}
+ }
+
+
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{
+ data.rule.configuration.threshold.min | percent: '1.2-2'
+ }}
+ } @else {
+ {{ data.rule.configuration.threshold.min }}
+ }
+
+
+
+ @if (data.rule.configuration.threshold.unit === '%') {
+ {{
+ data.rule.configuration.threshold.max | percent: '1.2-2'
+ }}
+ } @else {
+ {{ data.rule.configuration.threshold.max }}
+ }
+
+
+ }
diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss
index dc9093b4..0f6fce3d 100644
--- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss
+++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss
@@ -1,2 +1,5 @@
:host {
+ label {
+ margin-bottom: 0;
+ }
}
From 9d8f116dd14e0e73489a2f1e6de7604c069abf98 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 16 Nov 2024 13:25:48 +0100
Subject: [PATCH 34/65] Feature/upgrade prisma to version 5.22.0 (#4047)
* Upgrade prisma to version 5.22.0
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 64 +++++++++++++++++++++++------------------------
package.json | 4 +--
3 files changed, 35 insertions(+), 34 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89857f9b..58f0d6ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the usability to customize the rule thresholds in the _X-ray_ page by introducing range sliders (experimental)
- Improved the language localization for Italian (`it`)
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`
+- Upgraded `prisma` from version `5.21.1` to `5.22.0`
- Upgraded `uuid` from version `9.0.1` to `11.0.2`
## 2.122.0 - 2024-11-07
diff --git a/package-lock.json b/package-lock.json
index f6351537..be881d36 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,7 +40,7 @@
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
- "@prisma/client": "5.21.1",
+ "@prisma/client": "5.22.0",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
"@stripe/stripe-js": "4.9.0",
@@ -150,7 +150,7 @@
"nx": "20.0.6",
"prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0",
- "prisma": "5.21.1",
+ "prisma": "5.22.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "7.0.1",
@@ -8329,9 +8329,9 @@
"license": "MIT"
},
"node_modules/@prisma/client": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz",
- "integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
+ "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@@ -8347,53 +8347,53 @@
}
},
"node_modules/@prisma/debug": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz",
- "integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
+ "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz",
- "integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
+ "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "5.21.1",
- "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
- "@prisma/fetch-engine": "5.21.1",
- "@prisma/get-platform": "5.21.1"
+ "@prisma/debug": "5.22.0",
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
+ "@prisma/fetch-engine": "5.22.0",
+ "@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/engines-version": {
- "version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz",
- "integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==",
+ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
+ "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz",
- "integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
+ "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "5.21.1",
- "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
- "@prisma/get-platform": "5.21.1"
+ "@prisma/debug": "5.22.0",
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
+ "@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/get-platform": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz",
- "integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
+ "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/debug": "5.21.1"
+ "@prisma/debug": "5.22.0"
}
},
"node_modules/@redis/bloom": {
@@ -29385,14 +29385,14 @@
}
},
"node_modules/prisma": {
- "version": "5.21.1",
- "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz",
- "integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==",
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
+ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@prisma/engines": "5.21.1"
+ "@prisma/engines": "5.22.0"
},
"bin": {
"prisma": "build/index.js"
diff --git a/package.json b/package.json
index 9fa4d045..34f5a4fa 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
- "@prisma/client": "5.21.1",
+ "@prisma/client": "5.22.0",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
"@stripe/stripe-js": "4.9.0",
@@ -196,7 +196,7 @@
"nx": "20.0.6",
"prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0",
- "prisma": "5.21.1",
+ "prisma": "5.22.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"replace-in-file": "7.0.1",
From 0e7482938b29203b390d1d6ea707bf20b04a6ab6 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 16 Nov 2024 18:21:22 +0100
Subject: [PATCH 35/65] Feature/add black weeks 2024 blog post (#4049)
* Add Black Weeks 2024 blog post
* Update changelog
---
CHANGELOG.md | 4 +
apps/api/src/assets/sitemap.xml | 4 +
.../middlewares/html-template.middleware.ts | 4 +
.../black-weeks-2024-page.component.ts | 17 ++
.../black-weeks-2024-page.html | 180 ++++++++++++++++++
.../pages/blog/blog-page-routing.module.ts | 27 ++-
apps/client/src/app/pages/blog/blog-page.html | 26 +++
.../assets/images/blog/black-weeks-2024.jpg | Bin 0 -> 311715 bytes
8 files changed, 253 insertions(+), 9 deletions(-)
create mode 100644 apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts
create mode 100644 apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html
create mode 100644 apps/client/src/assets/images/blog/black-weeks-2024.jpg
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58f0d6ab..c136e5f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+- Added a blog post: _Black Weeks 2024_
+
### Changed
- Moved the chart of the holdings tab on the home page from experimental to general availability
diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml
index 3a0f44ff..5a49f671 100644
--- a/apps/api/src/assets/sitemap.xml
+++ b/apps/api/src/assets/sitemap.xml
@@ -188,6 +188,10 @@
https://ghostfol.io/en/blog/2024/09/hacktoberfest-2024
${currentDate}T00:00:00+00:00
+
+ https://ghostfol.io/en/blog/2024/11/black-weeks-2024
+ ${currentDate}T00:00:00+00:00
+
https://ghostfol.io/en/faq
${currentDate}T00:00:00+00:00
diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts
index 7b7cb09f..6c929c38 100644
--- a/apps/api/src/middlewares/html-template.middleware.ts
+++ b/apps/api/src/middlewares/html-template.middleware.ts
@@ -87,6 +87,10 @@ const locales = {
'/en/blog/2024/09/hacktoberfest-2024': {
featureGraphicPath: 'assets/images/blog/hacktoberfest-2024.png',
title: `Hacktoberfest 2024 - ${title}`
+ },
+ '/en/blog/2024/11/black-weeks-2024': {
+ featureGraphicPath: 'assets/images/blog/black-weeks-2024.jpg',
+ title: `Black Weeks 2024 - ${title}`
}
};
diff --git a/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts
new file mode 100644
index 00000000..5b380a3c
--- /dev/null
+++ b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.component.ts
@@ -0,0 +1,17 @@
+import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
+
+import { Component } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterModule } from '@angular/router';
+
+@Component({
+ host: { class: 'page' },
+ imports: [GfPremiumIndicatorComponent, MatButtonModule, RouterModule],
+ selector: 'gf-black-weeks-2024-page',
+ standalone: true,
+ templateUrl: './black-weeks-2024-page.html'
+})
+export class BlackWeeks2024PageComponent {
+ public routerLinkFeatures = ['/' + $localize`:snake-case:features`];
+ public routerLinkPricing = ['/' + $localize`:snake-case:pricing`];
+}
diff --git a/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html
new file mode 100644
index 00000000..aeac1cf1
--- /dev/null
+++ b/apps/client/src/app/pages/blog/2024/11/black-weeks-2024/black-weeks-2024-page.html
@@ -0,0 +1,180 @@
+
+
+
+
+
+
Black Weeks 2024
+
2024-11-16
+
+
+
+
+ Take advantage of our exclusive Black Weeks offer
+ and save 25% on your annual
+ Ghostfolio Premium
+
+
+ subscription, plus get 3 months extra for free!
+
+
+
+
+ Ghostfolio
+ is a powerful personal finance dashboard, designed to simplify your
+ investment journey. With this Open Source Software (OSS) platform,
+ you can:
+
+
+
+ Unify your assets : Track your financial
+ portfolio, including stocks, ETFs, cryptocurrencies, etc.
+
+
+ Gain deeper insights : Access real-time analytics
+ and data-driven insights.
+
+
+ Make informed decisions : Empower yourself with
+ actionable information.
+
+
+
+
+
+ Don’t miss this limited-time offer to optimize your financial
+ future.
+
+
+ Get the Deal
+
+
+ For more information, visit our
+ pricing page .
+
+
+
+
+
+ 2024
+
+
+ Black Friday
+
+
+ Black Weeks
+
+
+ Cryptocurrency
+
+
+ Dashboard
+
+
+ Deal
+
+
+ DeFi
+
+
+ ETF
+
+
+ Finance
+
+
+ Fintech
+
+
+ Ghostfolio
+
+
+ Ghostfolio Premium
+
+
+ Hosting
+
+
+ Investment
+
+
+ Open Source
+
+
+ OSS
+
+
+ Personal Finance
+
+
+ Portfolio
+
+
+ Portfolio Tracker
+
+
+ Pricing
+
+
+ Promotion
+
+
+ SaaS
+
+
+ Sale
+
+
+ Software
+
+
+ Stock
+
+
+ Subscription
+
+
+ Wealth
+
+
+ Wealth Management
+
+
+ Web3
+
+
+ Web 3.0
+
+
+
+
+
+
+ Blog
+
+
+ Black Weeks 2024
+
+
+
+
+
+
+
diff --git a/apps/client/src/app/pages/blog/blog-page-routing.module.ts b/apps/client/src/app/pages/blog/blog-page-routing.module.ts
index 3c28543e..d6c510c2 100644
--- a/apps/client/src/app/pages/blog/blog-page-routing.module.ts
+++ b/apps/client/src/app/pages/blog/blog-page-routing.module.ts
@@ -165,15 +165,6 @@ const routes: Routes = [
).then((c) => c.Hacktoberfest2023PageComponent),
title: 'Hacktoberfest 2023'
},
- {
- canActivate: [AuthGuard],
- path: '2023/11/hacktoberfest-2023-debriefing',
- loadComponent: () =>
- import(
- './2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component'
- ).then((c) => c.Hacktoberfest2023DebriefingPageComponent),
- title: 'Hacktoberfest 2023 Debriefing'
- },
{
canActivate: [AuthGuard],
path: '2023/11/black-week-2023',
@@ -183,6 +174,15 @@ const routes: Routes = [
),
title: 'Black Week 2023'
},
+ {
+ canActivate: [AuthGuard],
+ path: '2023/11/hacktoberfest-2023-debriefing',
+ loadComponent: () =>
+ import(
+ './2023/11/hacktoberfest-2023-debriefing/hacktoberfest-2023-debriefing-page.component'
+ ).then((c) => c.Hacktoberfest2023DebriefingPageComponent),
+ title: 'Hacktoberfest 2023 Debriefing'
+ },
{
canActivate: [AuthGuard],
path: '2024/09/hacktoberfest-2024',
@@ -191,6 +191,15 @@ const routes: Routes = [
'./2024/09/hacktoberfest-2024/hacktoberfest-2024-page.component'
).then((c) => c.Hacktoberfest2024PageComponent),
title: 'Hacktoberfest 2024'
+ },
+ {
+ canActivate: [AuthGuard],
+ path: '2024/11/black-weeks-2024',
+ loadComponent: () =>
+ import('./2024/11/black-weeks-2024/black-weeks-2024-page.component').then(
+ (c) => c.BlackWeeks2024PageComponent
+ ),
+ title: 'Black Weeks 2024'
}
];
diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html
index 73b4b090..babeec4c 100644
--- a/apps/client/src/app/pages/blog/blog-page.html
+++ b/apps/client/src/app/pages/blog/blog-page.html
@@ -8,6 +8,32 @@
finance
+ @if (hasPermissionForSubscription) {
+
+
+
+
+
+ }
diff --git a/apps/client/src/assets/images/blog/black-weeks-2024.jpg b/apps/client/src/assets/images/blog/black-weeks-2024.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c71827c5ef5660d110c21275d432846f8d6cfe72
GIT binary patch
literal 311715
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