Browse Source

Merge branch 'main' into task/migrate-watchlist-to-dedicated-routes

pull/7837/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
a1472a4528
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 1
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  3. 136
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  4. 37
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 8
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  6. 4
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  7. 59
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  8. 11
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  9. 4
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  10. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  11. 4
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  12. 5
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  13. 7
      apps/client/src/assets/oss-friends.json
  14. 4
      libs/ui/src/lib/assistant/assistant.component.ts
  15. 60
      package-lock.json
  16. 8
      package.json

4
CHANGELOG.md

@ -9,7 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the loading state of the activity count in the portfolio summary
- Migrated the create dialog of the watchlist to a dedicated route - Migrated the create dialog of the watchlist to a dedicated route
- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types
- Upgraded `bull-board` from version `9.0.1` to `9.5.0`
- Upgraded `zod` from version `4.4.3` to `4.5.4`
## 3.68.0 - 2026-09-06 ## 3.68.0 - 2026-09-06

1
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -10,6 +10,7 @@ import { UseFilters } from '@nestjs/common';
import { Payload } from '@nestjs/microservices'; import { Payload } from '@nestjs/microservices';
import { McpController, Tool } from '@rekog/mcp-nest'; import { McpController, Tool } from '@rekog/mcp-nest';
import { z } from 'zod'; import { z } from 'zod';
import 'zod/compile';
import { import {
GET_ACCOUNTS_PARAMETERS, GET_ACCOUNTS_PARAMETERS,

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

@ -355,8 +355,13 @@ describe('PortfolioService', () => {
describe('getDetails', () => { describe('getDetails', () => {
const setUpCashOnlyPortfolio = ({ const setUpCashOnlyPortfolio = ({
baseCurrency = 'CHF', baseCurrency = 'CHF',
emergencyFund emergencyFund,
}: { baseCurrency?: string; emergencyFund?: number } = {}) => { quantity = 2000
}: {
baseCurrency?: string;
emergencyFund?: number;
quantity?: number;
} = {}) => {
const cashAccount: AccountWithBalance = { const cashAccount: AccountWithBalance = {
balance: 2000, balance: 2000,
comment: null, comment: null,
@ -421,7 +426,7 @@ describe('PortfolioService', () => {
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {}, netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceWithCurrencyEffectMap: {}, netPerformanceWithCurrencyEffectMap: {},
quantity: new Big(2000), quantity: new Big(quantity),
symbol: 'USD', symbol: 'USD',
tags: [], tags: [],
timeWeightedInvestment: new Big(0), timeWeightedInvestment: new Big(0),
@ -493,6 +498,131 @@ describe('PortfolioService', () => {
expect(holdings[0].assetProfile.symbol).toBe('USD'); expect(holdings[0].assetProfile.symbol).toBe('USD');
expect(holdings[0].valueInBaseCurrency).toBe(1000); expect(holdings[0].valueInBaseCurrency).toBe(1000);
}); });
it('should include closed holdings when all holdings are requested', async () => {
setUpCashOnlyPortfolio({ quantity: 0 });
const { holdings } = await portfolioService.getDetails({
filters: [],
includeAllHoldings: true,
userId: userDummyData.id
});
expect(holdings).toHaveLength(1);
expect(holdings[0].quantity).toBe(0);
});
it.each([
{ holdingType: 'ACTIVE', quantity: 2000 },
{ holdingType: 'CLOSED', quantity: 0 }
])(
'should return $holdingType holdings when the holding type is specified',
async ({ holdingType, quantity }) => {
setUpCashOnlyPortfolio({ quantity });
const { holdings } = await portfolioService.getDetails({
filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
userId: userDummyData.id
});
expect(holdings).toHaveLength(1);
expect(holdings[0].quantity).toBe(quantity);
}
);
it('should remove the holding type only from the snapshot filters', async () => {
setUpCashOnlyPortfolio({ quantity: 0 });
await portfolioService.getDetails({
filters: [
{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' },
{ id: 'CLOSED', type: 'HOLDING_TYPE' }
],
userId: userDummyData.id
});
expect(portfolioCalculatorFactory.createCalculator).toHaveBeenCalledWith(
expect.objectContaining({
filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }]
})
);
expect(
activitiesService.getActivitiesForPortfolioCalculator
).toHaveBeenCalledWith({
filters: [{ id: AssetClass.EQUITY, type: 'ASSET_CLASS' }],
userCurrency: 'CHF',
userId: userDummyData.id
});
});
});
describe('getHoldings', () => {
const activeHolding = {
assetProfile: {
isin: 'US0378331005',
name: 'Apple',
symbol: 'AAPL'
},
quantity: 1
};
const closedHolding = {
assetProfile: {
isin: 'US5949181045',
name: 'Microsoft',
symbol: 'MSFT'
},
quantity: 0
};
beforeEach(() => {
jest.spyOn(portfolioService, 'getDetails').mockResolvedValue({
holdings: [activeHolding, closedHolding]
} as unknown as Awaited<ReturnType<typeof portfolioService.getDetails>>);
});
it('should request all holdings when the holding type is not specified', async () => {
const holdings = await portfolioService.getHoldings({
dateRange: 'max',
userId: userDummyData.id
});
expect(holdings).toEqual([activeHolding, closedHolding]);
expect(portfolioService.getDetails).toHaveBeenCalledWith({
dateRange: 'max',
filters: undefined,
includeAllHoldings: true,
userId: userDummyData.id
});
});
it('should find a closed holding when the holding type is not specified', async () => {
const holdings = await portfolioService.getHoldings({
dateRange: 'max',
filters: [{ id: 'Microsoft', type: 'SEARCH_QUERY' }],
userId: userDummyData.id
});
expect(holdings).toEqual([closedHolding]);
});
it.each(['ACTIVE', 'CLOSED'])(
'should not request all holdings when the holding type is %s',
async (holdingType) => {
await portfolioService.getHoldings({
dateRange: 'max',
filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
userId: userDummyData.id
});
expect(portfolioService.getDetails).toHaveBeenCalledWith({
dateRange: 'max',
filters: [{ id: holdingType, type: 'HOLDING_TYPE' }],
includeAllHoldings: false,
userId: userDummyData.id
});
}
);
}); });
describe('getHolding', () => { describe('getHolding', () => {

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

@ -477,12 +477,12 @@ export class PortfolioService {
filters?: Filter[]; filters?: Filter[];
userId: string; userId: string;
}) { }) {
const { SEARCH_QUERY: [filterBySearchQuery] = [] } = groupBy( const {
filters, HOLDING_TYPE: [filterByHoldingType] = [],
({ type }) => { SEARCH_QUERY: [filterBySearchQuery] = []
return type; } = groupBy(filters, ({ type }) => {
} return type;
); });
const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => { const filtersWithoutSearchQueryFilter = filters?.filter(({ type }) => {
return type !== 'SEARCH_QUERY'; return type !== 'SEARCH_QUERY';
@ -491,7 +491,8 @@ export class PortfolioService {
let { holdings } = await this.getDetails({ let { holdings } = await this.getDetails({
dateRange, dateRange,
userId, userId,
filters: filtersWithoutSearchQueryFilter filters: filtersWithoutSearchQueryFilter,
includeAllHoldings: !filterByHoldingType
}); });
if (filterBySearchQuery) { if (filterBySearchQuery) {
@ -592,6 +593,7 @@ export class PortfolioService {
public async getDetails({ public async getDetails({
dateRange = DEFAULT_DATE_RANGE, dateRange = DEFAULT_DATE_RANGE,
filters, filters,
includeAllHoldings = false,
user: userFromCaller, user: userFromCaller,
userId, userId,
withExcludedAccounts = false, withExcludedAccounts = false,
@ -600,6 +602,7 @@ export class PortfolioService {
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
filters?: Filter[]; filters?: Filter[];
includeAllHoldings?: boolean;
user?: UserWithSettings; user?: UserWithSettings;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
@ -614,19 +617,23 @@ export class PortfolioService {
(user.settings?.settings as UserSettings)?.emergencyFund ?? 0 (user.settings?.settings as UserSettings)?.emergencyFund ?? 0
); );
const portfolioSnapshotFilters = filters?.filter(({ type }) => {
return type !== 'HOLDING_TYPE';
});
const { activities } = const { activities } =
await this.activitiesService.getActivitiesForPortfolioCalculator({ await this.activitiesService.getActivitiesForPortfolioCalculator({
filters,
userCurrency, userCurrency,
userId userId,
filters: portfolioSnapshotFilters
}); });
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
filters,
userId, userId,
calculationType: this.getUserPerformanceCalculationType(user), calculationType: this.getUserPerformanceCalculationType(user),
currency: userCurrency currency: userCurrency,
filters: portfolioSnapshotFilters
}); });
const { createdAt, currentValueInBaseCurrency, hasErrors, positions } = const { createdAt, currentValueInBaseCurrency, hasErrors, positions } =
@ -706,13 +713,13 @@ export class PortfolioService {
tags, tags,
valueInBaseCurrency valueInBaseCurrency
} of positions) { } of positions) {
if (isFilteredByClosedHoldings === true) { if (!includeAllHoldings) {
if (!quantity.eq(0)) { if (isFilteredByClosedHoldings && !quantity.eq(0)) {
// Ignore positions with a quantity // Ignore positions with a quantity
continue; continue;
} }
} else {
if (quantity.eq(0)) { if (!isFilteredByClosedHoldings && quantity.eq(0)) {
// Ignore positions without any quantity // Ignore positions without any quantity
continue; continue;
} }

8
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -393,8 +393,12 @@ export class GfAccountDetailDialogComponent implements OnInit {
.fetchPortfolioHoldings({ .fetchPortfolioHoldings({
filters: [ filters: [
{ {
type: 'ACCOUNT', id: this.data.accountId,
id: this.data.accountId type: 'ACCOUNT'
},
{
id: 'ACTIVE',
type: 'HOLDING_TYPE'
} }
] ]
}) })

4
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -153,9 +153,7 @@ export class GfHomeHoldingsComponent implements OnInit {
private fetchHoldings() { private fetchHoldings() {
const filters = this.userService.getFilters(); const filters = this.userService.getFilters();
if (this.holdingType === 'CLOSED') { filters.push({ id: this.holdingType, type: 'HOLDING_TYPE' });
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
}
return this.dataService.fetchPortfolioHoldings({ return this.dataService.fetchPortfolioHoldings({
filters, filters,

59
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -9,25 +9,33 @@
/> />
</div> </div>
</div> </div>
<div @if (isLoading) {
class="flex-nowrap px-3 py-1 row" <div class="flex-nowrap px-3 py-1 row">
[hidden]="summary?.activityCount === null" <div class="d-flex flex-grow-1 indent-1 text-truncate">
> <ngx-skeleton-loader
<div class="d-flex flex-grow-1 indent-1 text-truncate"> animation="pulse"
{{ summary?.activityCount }} [theme]="{ height: '1.5rem', width: '7.5rem' }"
<ng-container i18n>{summary?.activityCount, plural, />
=1 {activity} </div>
other {activities}
}</ng-container>
<span
class="align-items-center d-none d-sm-flex ml-1"
matTooltipPosition="above"
[matTooltip]="buyAndSellActivitiesTooltip"
>
<ion-icon name="information-circle-outline" />
</span>
</div> </div>
</div> } @else {
<div class="flex-nowrap px-3 py-1 row">
<div class="d-flex flex-grow-1 indent-1 text-truncate">
{{ summary?.activityCount }}
<ng-container i18n>{summary?.activityCount, plural,
=1 {activity}
other {activities}
}</ng-container>
<span
class="align-items-center d-none d-sm-flex ml-1"
matTooltipPosition="above"
[matTooltip]="buyAndSellActivitiesTooltip"
>
<ion-icon name="information-circle-outline" />
</span>
</div>
</div>
}
<div class="row"> <div class="row">
<div class="col"><hr /></div> <div class="col"><hr /></div>
</div> </div>
@ -127,11 +135,16 @@
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 indent-1 text-truncate"> <div class="flex-grow-1 indent-1 text-truncate">
<ng-container i18n>Net Performance</ng-container> <ng-container i18n>Net Performance</ng-container>
<abbr @if (
class="d-none d-sm-inline-block initialism ml-2 text-muted" user?.settings?.performanceCalculationType ===
title="Return on Average Investment" PerformanceCalculationType.ROAI
>(ROAI)</abbr ) {
> <abbr
class="d-none d-sm-inline-block initialism ml-2 text-muted"
title="Return on Average Investment"
>(ROAI)</abbr
>
}
</div> </div>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value

11
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -1,6 +1,7 @@
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper'; import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -23,10 +24,16 @@ import {
informationCircleOutline informationCircleOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [GfValueComponent, IonIcon, MatTooltipModule], imports: [
GfValueComponent,
IonIcon,
MatTooltipModule,
NgxSkeletonLoaderModule
],
selector: 'gf-portfolio-summary', selector: 'gf-portfolio-summary',
styleUrls: ['./portfolio-summary.component.scss'], styleUrls: ['./portfolio-summary.component.scss'],
templateUrl: './portfolio-summary.component.html' templateUrl: './portfolio-summary.component.html'
@ -48,6 +55,8 @@ export class GfPortfolioSummaryComponent implements OnChanges {
'BUY_AND_SELL_ACTIVITIES_TOOLTIP' 'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
); );
protected readonly PerformanceCalculationType = PerformanceCalculationType;
protected isCashExpanded = false; protected isCashExpanded = false;
protected isHoldingsExpanded = false; protected isHoldingsExpanded = false;
protected precision = 2; protected precision = 2;

4
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -367,7 +367,9 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
private loadHoldings() { private loadHoldings() {
this.dataService this.dataService
.fetchPortfolioHoldings() .fetchPortfolioHoldings({
filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
})
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = getHoldingsForFilter(holdings); this.holdings = getHoldingsForFilter(holdings);

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -153,7 +153,9 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.defaultDateFormat = getDateFormatString(this.locale); this.defaultDateFormat = getDateFormatString(this.locale);
this.dataService this.dataService
.fetchPortfolioHoldings() .fetchPortfolioHoldings({
filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
})
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.defaultLookupItems = holdings this.defaultLookupItems = holdings

4
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -144,6 +144,10 @@ export class GfImportActivitiesDialogComponent {
{ {
id: AssetClass.FIXED_INCOME, id: AssetClass.FIXED_INCOME,
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
},
{
id: 'ACTIVE',
type: 'HOLDING_TYPE'
} }
], ],
range: DEFAULT_DATE_RANGE range: DEFAULT_DATE_RANGE

5
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -377,7 +377,10 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchPortfolioHoldings({ .fetchPortfolioHoldings({
filters: this.userService.getFilters(), filters: [
...this.userService.getFilters(),
{ id: 'ACTIVE', type: 'HOLDING_TYPE' }
],
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

7
apps/client/src/assets/oss-friends.json

@ -1,5 +1,5 @@
{ {
"createdAt": "2026-05-26T00:00:00.000Z", "createdAt": "2026-09-07T00:00:00.000Z",
"data": [ "data": [
{ {
"name": "Activepieces", "name": "Activepieces",
@ -76,11 +76,6 @@
"description": "Onyx is the open-source AI chat connected to your docs, apps, and people.", "description": "Onyx is the open-source AI chat connected to your docs, apps, and people.",
"href": "https://onyx.app" "href": "https://onyx.app"
}, },
{
"name": "OpenBB",
"description": "Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.",
"href": "https://openbb.co"
},
{ {
"name": "OpenStatus", "name": "OpenStatus",
"description": "Open-source monitoring platform with beautiful status pages", "description": "Open-source monitoring platform with beautiful status pages",

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

@ -479,7 +479,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
this.setIsOpen(true); this.setIsOpen(true);
this.dataService this.dataService
.fetchPortfolioHoldings() .fetchPortfolioHoldings({
filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }]
})
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = getHoldingsForFilter(holdings); this.holdings = getHoldingsForFilter(holdings);

60
package-lock.json

@ -21,9 +21,9 @@
"@angular/platform-browser-dynamic": "22.1.2", "@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2", "@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2", "@angular/service-worker": "22.1.2",
"@bull-board/api": "9.0.1", "@bull-board/api": "9.5.0",
"@bull-board/express": "9.0.1", "@bull-board/express": "9.5.0",
"@bull-board/nestjs": "9.0.1", "@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1", "@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7", "@internationalized/number": "3.6.7",
@ -105,7 +105,7 @@
"undici": "8.10.0", "undici": "8.10.0",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.4.3", "zod": "4.5.4",
"zone.js": "0.16.2" "zone.js": "0.16.2"
}, },
"devDependencies": { "devDependencies": {
@ -3378,6 +3378,16 @@
"node": ">=22.13.0" "node": ">=22.13.0"
} }
}, },
"node_modules/@angular/cli/node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@angular/common": { "node_modules/@angular/common": {
"version": "22.1.2", "version": "22.1.2",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-22.1.2.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.1.2.tgz",
@ -6156,17 +6166,17 @@
"license": "(Apache-2.0 AND BSD-3-Clause)" "license": "(Apache-2.0 AND BSD-3-Clause)"
}, },
"node_modules/@bull-board/api": { "node_modules/@bull-board/api": {
"version": "9.0.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.0.1.tgz", "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-9.5.0.tgz",
"integrity": "sha512-QZ+SDOlD1MAePNqy8qfs+7zExZyJCW/+nEncGE6Mt/+MlxT9mqvyZWmPUm38QZvIBJGSKaA9i9ZElR9wDbbPhw==", "integrity": "sha512-fmOyOy1lZiLZFyDJjI2osZrpTMO+JSgmGNhDf3MrmOf3oF084o6w0wpGefs0OqEfyUT1F5/1arE5AwmEUuxZ+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"redis-info": "^3.1.0" "redis-info": "^3.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"@bull-board/ui": "9.0.1", "@bull-board/ui": "9.5.0",
"bull": "^4.16.5", "bull": "^4.16.5",
"bullmq": "^5.79.2 || ^6.0.0" "bullmq": "^5.56.0 || ^6.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"bull": { "bull": {
@ -6178,13 +6188,13 @@
} }
}, },
"node_modules/@bull-board/express": { "node_modules/@bull-board/express": {
"version": "9.0.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.0.1.tgz", "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-9.5.0.tgz",
"integrity": "sha512-eblNP2imr639K9KG0n1sBaJp6lw6pu4/j7q3W+bydxtw83v4wobCcuVUxuhQkRaLTrIUMR/Snulc7+/vtxCshw==", "integrity": "sha512-EKeXQzeb1zF9ra0hfYfPArU1JlWKl2vyL4Rb3gQW0XzU7SkEFgYm3727gDrQZdU8dvc5g/FLEqc0dr6UkDUHhg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bull-board/api": "9.0.1", "@bull-board/api": "9.5.0",
"@bull-board/ui": "9.0.1", "@bull-board/ui": "9.5.0",
"ejs": "^6.0.1", "ejs": "^6.0.1",
"express": "^5.2.1" "express": "^5.2.1"
} }
@ -6202,12 +6212,12 @@
} }
}, },
"node_modules/@bull-board/nestjs": { "node_modules/@bull-board/nestjs": {
"version": "9.0.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.0.1.tgz", "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-9.5.0.tgz",
"integrity": "sha512-pVcMZyuZ/X8SLjoayW8aREM1f4RFdI9WlwvHmT9RwlPhRnFLTnGMbnC5LOk+cYR/9WTB/Oeo1SDnzblIR99JtQ==", "integrity": "sha512-0P/Ru3cbOnf6GwnDGv4iPJ/NEjbcaicdpYMISpp6K7JZ24RVkCnmoUIKt8KbQdjZGqfH2EWmyIRC2WhiUE3T+g==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@bull-board/api": "^9.0.1", "@bull-board/api": "^9.5.0",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0", "@nestjs/bull-shared": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
@ -6216,12 +6226,12 @@
} }
}, },
"node_modules/@bull-board/ui": { "node_modules/@bull-board/ui": {
"version": "9.0.1", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.0.1.tgz", "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-9.5.0.tgz",
"integrity": "sha512-yGg0CMAFCA49IM1zPS+JFvUJW/whftaLtRyMWiVtvyLRd53uX0QW/uTaPcHy0V4bApp6U0FTa9+L9rk/49ciwg==", "integrity": "sha512-qrq0e906M8Ps/mV3bQXmj7EXJWLaFs5mNz+oBoLR/7EShBi5YooROzQ/SEFFedg/L9h6QWDWJ9Ph0AH7NrG9uw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bull-board/api": "9.0.1" "@bull-board/api": "9.5.0"
} }
}, },
"node_modules/@cacheable/utils": { "node_modules/@cacheable/utils": {
@ -37225,9 +37235,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.4.3", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

8
package.json

@ -65,9 +65,9 @@
"@angular/platform-browser-dynamic": "22.1.2", "@angular/platform-browser-dynamic": "22.1.2",
"@angular/router": "22.1.2", "@angular/router": "22.1.2",
"@angular/service-worker": "22.1.2", "@angular/service-worker": "22.1.2",
"@bull-board/api": "9.0.1", "@bull-board/api": "9.5.0",
"@bull-board/express": "9.0.1", "@bull-board/express": "9.5.0",
"@bull-board/nestjs": "9.0.1", "@bull-board/nestjs": "9.5.0",
"@codewithdan/observable-store": "2.2.15", "@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1", "@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.7", "@internationalized/number": "3.6.7",
@ -149,7 +149,7 @@
"undici": "8.10.0", "undici": "8.10.0",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.4.3", "zod": "4.5.4",
"zone.js": "0.16.2" "zone.js": "0.16.2"
}, },
"devDependencies": { "devDependencies": {

Loading…
Cancel
Save