From 30643862ceccc0e3722c60973317257517e5243a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 May 2026 17:47:19 +0300 Subject: [PATCH 1/8] Task/upgrade @bull-board dependencies to version 7.1.5 (#6877) * Upgrade @bull-board to version 7.1.5 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 6 +++--- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8a33af2a..fb221493e 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 - Extracted the page tabs to a reusable component +- Upgraded `bull-board` from version `7.0.0` to `7.1.5` ### Fixed diff --git a/package-lock.json b/package-lock.json index 6cdce7353..f45993a67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "7.0.0", - "@bull-board/express": "7.0.0", - "@bull-board/nestjs": "7.0.0", + "@bull-board/api": "7.1.5", + "@bull-board/express": "7.1.5", + "@bull-board/nestjs": "7.1.5", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.6", @@ -3522,25 +3522,25 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bull-board/api": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.0.0.tgz", - "integrity": "sha512-ISNspLHVmUWUSq/eLw+wd1FuBBUnqpLbYP2xUNmehpfKhS+NoZWMbBvqjUYVeE/HLfUkRcR1edzMKpl5n9zlSw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.1.5.tgz", + "integrity": "sha512-EW0sbTtGIysu9vipdVpPQeToPqOpPgVZTt+pn1Ut3gbSS/GLWbEgIfFtMmSQDUoSL9WH00RzjgUY5K+43nWh0A==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "7.0.0" + "@bull-board/ui": "7.1.5" } }, "node_modules/@bull-board/express": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.0.0.tgz", - "integrity": "sha512-3Tc/EyU5PQMTcTzcafFSrmRDiEbJBEU/EaVQ5OVYcuJ7DZCp5Pkvm0/2VtaCe2uywdtwn0ZaynlSIpB27FKX6A==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-7.1.5.tgz", + "integrity": "sha512-kp4SzhVjZlykryiQwcOhJjDhiLbBnZoAMoSgEstzqQ0raLw+jERRC6ryJ0MIQO+SO+Jv9EjjxrXCR8O2YSP/eg==", "license": "MIT", "dependencies": { - "@bull-board/api": "7.0.0", - "@bull-board/ui": "7.0.0", + "@bull-board/api": "7.1.5", + "@bull-board/ui": "7.1.5", "ejs": "^5.0.2", "express": "^5.2.1" } @@ -3558,12 +3558,12 @@ } }, "node_modules/@bull-board/nestjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.0.0.tgz", - "integrity": "sha512-ypXm0eJHIMQzjN+3fjf84cVxugBg/K4Bpo0eYcV4u/AsteR/dnr6e7F79ICRgg1WWoczqmSMl0JhlmykpyhAMg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-7.1.5.tgz", + "integrity": "sha512-1y+HkjnDaZoSCXJRsiYfBNBVx+PX3I8x3Uv+SSJuSpt2vHifMRwFbChO3XDxeWXetT1eR+yqPVq6ub5eJwNOYQ==", "license": "MIT", "peerDependencies": { - "@bull-board/api": "^7.0.0", + "@bull-board/api": "^7.1.5", "@nestjs/bull-shared": "^10.0.0 || ^11.0.0", "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", @@ -3572,12 +3572,12 @@ } }, "node_modules/@bull-board/ui": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.0.0.tgz", - "integrity": "sha512-AnKeklpDn0iMFgu4ukDU6uTNmw4oudl07G4k2Fh95SknKDrXSiWRV0N1TGUawMqyfG1Yi5P/W/8d7raBq/Uw6w==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.1.5.tgz", + "integrity": "sha512-2IkatKwNRx/1M9/lAZIptcxS1FPNq6icpp2M46Upwd4olVxs/ujF9Kvs+Ff9ExtIO/OgYfwx7mG2IprGZ+nQCg==", "license": "MIT", "dependencies": { - "@bull-board/api": "7.0.0" + "@bull-board/api": "7.1.5" } }, "node_modules/@cacheable/utils": { diff --git a/package.json b/package.json index 69c695889..dbbe0c3af 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,9 @@ "@angular/platform-browser-dynamic": "21.2.7", "@angular/router": "21.2.7", "@angular/service-worker": "21.2.7", - "@bull-board/api": "7.0.0", - "@bull-board/express": "7.0.0", - "@bull-board/nestjs": "7.0.0", + "@bull-board/api": "7.1.5", + "@bull-board/express": "7.1.5", + "@bull-board/nestjs": "7.1.5", "@codewithdan/observable-store": "2.2.15", "@date-fns/utc": "2.1.1", "@internationalized/number": "3.6.6", From 5fcdd7fff87650f8b45c41f04811afee993c663d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 May 2026 17:48:06 +0300 Subject: [PATCH 2/8] Task/improve border style on pricing page (#6875) Improve border style --- apps/client/src/app/pages/pricing/pricing-page.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss index 64d4bba80..dc9317c81 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.scss +++ b/apps/client/src/app/pages/pricing/pricing-page.scss @@ -18,8 +18,12 @@ border-color 0.5s ease, box-shadow 0.5s ease; - &:hover, &.active { + border-color: currentColor; + box-shadow: 0 0 0 1px currentColor; + } + + &:hover { border-color: rgba(var(--palette-primary-500), 1); box-shadow: 0 0 0 1px rgba(var(--palette-primary-500), 1); } From 9ed4c679b2410907381ed388a4fd789b3d293375 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 May 2026 17:49:13 +0300 Subject: [PATCH 3/8] Task/enable Bull Dashboard in admin control panel without environment variable (#6876) * Enable Bull Dashboard without environment variable * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/app.module.ts | 42 +++++++++---------- apps/api/src/app/user/user.service.ts | 2 +- .../configuration/configuration.service.ts | 1 - .../interfaces/environment.interface.ts | 1 - .../data-gathering/data-gathering.module.ts | 20 ++++----- .../portfolio-snapshot.module.ts | 20 ++++----- .../statistics-gathering.module.ts | 2 +- 8 files changed, 38 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb221493e..228e9c377 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 +- Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental) - Extracted the page tabs to a reusable component - Upgraded `bull-board` from version `7.0.0` to `7.1.5` diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 3316f9ce4..4857c7e14 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -74,29 +74,25 @@ import { UserModule } from './user/user.module'; AuthDeviceModule, AuthModule, BenchmarksModule, - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forRoot({ - adapter: ExpressAdapter, - boardOptions: { - uiConfig: { - boardLogo: { - height: 0, - path: '', - width: 0 - }, - boardTitle: 'Job Queues', - favIcon: { - alternative: '/assets/favicon-32x32.png', - default: '/assets/favicon-32x32.png' - } - } - }, - middleware: BullBoardAuthMiddleware, - route: BULL_BOARD_ROUTE - }) - ] - : []), + BullBoardModule.forRoot({ + adapter: ExpressAdapter, + boardOptions: { + uiConfig: { + boardLogo: { + height: 0, + path: '', + width: 0 + }, + boardTitle: 'Job Queues', + favIcon: { + alternative: '/assets/favicon-32x32.png', + default: '/assets/favicon-32x32.png' + } + } + }, + middleware: BullBoardAuthMiddleware, + route: BULL_BOARD_ROUTE + }), BullModule.forRoot({ redis: { db: parseInt(process.env.REDIS_DB ?? '0', 10), diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0c73833f7..4ad22a043 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -532,7 +532,7 @@ export class UserService { } if (hasRole(user, Role.ADMIN)) { - if (this.configurationService.get('ENABLE_FEATURE_BULL_BOARD')) { + if ((user.settings.settings as UserSettings).isExperimentalFeatures) { currentPermissions.push(permissions.accessAdminControlBullBoard); } diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index ad8e84a99..b19508d3e 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -44,7 +44,6 @@ export class ConfigurationService { ENABLE_FEATURE_AUTH_GOOGLE: bool({ default: false }), ENABLE_FEATURE_AUTH_OIDC: bool({ default: false }), ENABLE_FEATURE_AUTH_TOKEN: bool({ default: true }), - ENABLE_FEATURE_BULL_BOARD: bool({ default: false }), ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }), ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: bool({ default: true }), ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 9664ae144..eb3ac86a3 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -20,7 +20,6 @@ export interface Environment extends CleanedEnvAccessors { ENABLE_FEATURE_AUTH_GOOGLE: boolean; ENABLE_FEATURE_AUTH_OIDC: boolean; ENABLE_FEATURE_AUTH_TOKEN: boolean; - ENABLE_FEATURE_BULL_BOARD: boolean; ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean; ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES: boolean; ENABLE_FEATURE_READ_ONLY_MODE: boolean; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts index d163f0d29..5672df5e8 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts @@ -19,18 +19,14 @@ import { DataGatheringProcessor } from './data-gathering.processor'; @Module({ imports: [ - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forFeature({ - adapter: BullAdapter, - name: DATA_GATHERING_QUEUE, - options: { - displayName: 'Data Gathering', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' - } - }) - ] - : []), + BullBoardModule.forFeature({ + adapter: BullAdapter, + name: DATA_GATHERING_QUEUE, + options: { + displayName: 'Data Gathering', + readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + } + }), BullModule.registerQueue({ limiter: { duration: ms('4 seconds'), diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index 1260f1cf0..c90f826f6 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -25,18 +25,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; imports: [ AccountBalanceModule, ActivitiesModule, - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' - ? [ - BullBoardModule.forFeature({ - adapter: BullAdapter, - name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, - options: { - displayName: 'Portfolio Snapshot Computation', - readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' - } - }) - ] - : []), + BullBoardModule.forFeature({ + adapter: BullAdapter, + name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, + options: { + displayName: 'Portfolio Snapshot Computation', + readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + } + }), BullModule.registerQueue({ name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, settings: { diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts index 1818dd4ec..60b963c69 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts @@ -13,7 +13,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service'; @Module({ exports: [BullModule, StatisticsGatheringService], imports: [ - ...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true' + ...(process.env.ENABLE_FEATURE_STATISTICS === 'true' ? [ BullBoardModule.forFeature({ adapter: BullAdapter, From 3a95d8e7dcb26e40d8012a5395ca20dbd89c0b1e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 May 2026 19:11:24 +0300 Subject: [PATCH 4/8] Task/add monitor id check for uptime statistics gathering (#6873) Add monitor id check --- .../statistics-gathering.processor.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts index 07fc32585..1312d49ea 100644 --- a/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts +++ b/apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts @@ -93,12 +93,25 @@ export class StatisticsGatheringProcessor { @Process(GATHER_STATISTICS_UPTIME_PROCESS_JOB_NAME) public async gatherUptimeStatistics() { + const monitorId = await this.propertyService.getByKey( + PROPERTY_BETTER_UPTIME_MONITOR_ID + ); + + if (!monitorId) { + Logger.log( + `Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`, + 'StatisticsGatheringProcessor' + ); + + return; + } + Logger.log( 'Uptime statistics gathering has been started', 'StatisticsGatheringProcessor' ); - const uptime = await this.getUptime(); + const uptime = await this.getUptime(monitorId); await this.propertyService.put({ key: PROPERTY_UPTIME, @@ -179,12 +192,8 @@ export class StatisticsGatheringProcessor { } } - private async getUptime(): Promise { + private async getUptime(monitorId: string): Promise { try { - const monitorId = await this.propertyService.getByKey( - PROPERTY_BETTER_UPTIME_MONITOR_ID - ); - const { data } = await fetch( `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( subDays(new Date(), 90), From 703ea3492ce1ef7254ca5ed13f3744b4ff31d7a0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 16 May 2026 19:51:43 +0300 Subject: [PATCH 5/8] Task/extract page tabs to dedicated component (part 2) (#6881) Fix fab container style on mobile --- libs/ui/src/lib/page-tabs/page-tabs.component.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/ui/src/lib/page-tabs/page-tabs.component.scss b/libs/ui/src/lib/page-tabs/page-tabs.component.scss index bed3596bb..182d2eca1 100644 --- a/libs/ui/src/lib/page-tabs/page-tabs.component.scss +++ b/libs/ui/src/lib/page-tabs/page-tabs.component.scss @@ -14,13 +14,13 @@ ) ); - .fab-container { - @media (max-width: 575.98px) { - bottom: 5rem; + ::ng-deep { + .fab-container { + @media (max-width: 575.98px) { + bottom: 5rem; + } } - } - ::ng-deep { .mat-mdc-tab-nav-panel { padding: 2rem 0; } From 859aa8b39b6d0393d5878e702c65bbe81605dfbb Mon Sep 17 00:00:00 2001 From: lil-goat <167018448+lil-goat@users.noreply.github.com> Date: Sun, 17 May 2026 02:07:53 +0800 Subject: [PATCH 6/8] Task/improve pagination for activities in holding detail dialog (#6874) * Improve pagination * Update changelog --- CHANGELOG.md | 1 + .../holding-detail-dialog.component.ts | 52 +++++++++++++------ .../holding-detail-dialog.html | 3 ++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 228e9c377..d46d92da9 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 pagination in the activities table of the holding detail dialog - Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental) - Extracted the page tabs to a reusable component - Upgraded `bull-board` from version `7.0.0` to `7.1.5` diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index bf4955e45..8c42e37ea 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -1,5 +1,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { + DEFAULT_PAGE_SIZE, NUMERICAL_PRECISION_THRESHOLD_3_FIGURES, NUMERICAL_PRECISION_THRESHOLD_5_FIGURES, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES @@ -48,6 +49,7 @@ import { MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { PageEvent } from '@angular/material/paginator'; import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; @@ -140,6 +142,8 @@ export class GfHoldingDetailDialogComponent implements OnInit { public netPerformancePercentWithCurrencyEffectPrecision = 2; public netPerformanceWithCurrencyEffect: number; public netPerformanceWithCurrencyEffectPrecision = 2; + public pageIndex = 0; + public pageSize = DEFAULT_PAGE_SIZE; public quantity: number; public quantityPrecision = 2; public reportDataGlitchMail: string; @@ -178,10 +182,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { } public ngOnInit() { - const filters: Filter[] = [ - { id: this.data.dataSource, type: 'DATA_SOURCE' }, - { id: this.data.symbol, type: 'SYMBOL' } - ]; + const filters = this.getActivityFilters(); this.holdingForm = this.formBuilder.group({ tags: [] as string[] @@ -240,18 +241,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.changeDetectorRef.markForCheck(); }); - this.dataService - .fetchActivities({ - filters, - sortColumn: this.sortColumn, - sortDirection: this.sortDirection - }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ activities }) => { - this.dataSource = new MatTableDataSource(activities); - - this.changeDetectorRef.markForCheck(); - }); + this.fetchActivities(filters); this.dataService .fetchHoldingDetail({ @@ -543,6 +533,12 @@ export class GfHoldingDetailDialogComponent implements OnInit { }); } + public onChangePage(page: PageEvent) { + this.pageIndex = page.pageIndex; + + this.fetchActivities(); + } + public onCloneActivity(aActivity: Activity) { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink, @@ -626,6 +622,23 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.dialogRef.close(); } + private fetchActivities(filters: Filter[] = this.getActivityFilters()) { + this.dataService + .fetchActivities({ + filters, + skip: this.pageIndex * this.pageSize, + sortColumn: this.sortColumn, + sortDirection: this.sortDirection, + take: this.pageSize + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ activities }) => { + this.dataSource = new MatTableDataSource(activities); + + this.changeDetectorRef.markForCheck(); + }); + } + private fetchMarketData() { this.dataService .fetchMarketDataBySymbol({ @@ -648,4 +661,11 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.changeDetectorRef.markForCheck(); }); } + + private getActivityFilters(): Filter[] { + return [ + { id: this.data.dataSource, type: 'DATA_SOURCE' }, + { id: this.data.symbol, type: 'SYMBOL' } + ]; + } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 7c1906ee5..4b04a0986 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -353,6 +353,8 @@ [hasPermissionToFilter]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" + [pageIndex]="pageIndex" + [pageSize]="pageSize" [showActions]=" !data.hasImpersonationId && data.hasPermissionToCreateActivity && @@ -367,6 +369,7 @@ (activityToClone)="onCloneActivity($event)" (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" + (pageChanged)="onChangePage($event)" /> From bd2ca4aacdf715f53acb0950f30d69531a7e80a2 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 17 May 2026 01:49:16 +0700 Subject: [PATCH 7/8] Task/support DIRECT_URL for Prisma CLI and seeding to support connection poolers (#6882) * Add support for DIRECT_URL * Update changelog --- .config/prisma.ts | 2 +- CHANGELOG.md | 4 ++++ README.md | 41 +++++++++++++++++++++-------------------- prisma/seed.mts | 2 +- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.config/prisma.ts b/.config/prisma.ts index a9e7bbf64..4556ba305 100644 --- a/.config/prisma.ts +++ b/.config/prisma.ts @@ -7,7 +7,7 @@ expand(config({ quiet: true })); export default defineConfig({ datasource: { - url: process.env.DATABASE_URL ?? '' + url: process.env.DIRECT_URL ?? process.env.DATABASE_URL }, migrations: { path: join(__dirname, '..', 'prisma', 'migrations'), diff --git a/CHANGELOG.md b/CHANGELOG.md index d46d92da9..1d696039b 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 the `DIRECT_URL` environment variable to enable direct database connections + ### Changed - Improved the pagination in the activities table of the holding detail dialog diff --git a/README.md b/README.md index 87710d610..8557d4330 100644 --- a/README.md +++ b/README.md @@ -85,26 +85,27 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c ### Supported Environment Variables -| Name | Type | Default Value | Description | -| --------------------------- | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | -| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | -| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | -| `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | -| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token | -| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | -| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | -| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | -| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | -| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | -| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | -| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | -| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | -| `REDIS_HOST` | `string` | | The host where _Redis_ is running | -| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | -| `REDIS_PORT` | `number` | | The port where _Redis_ is running | -| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | -| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | +| Name | Type | Default Value | Description | +| --------------------------- | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | +| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | +| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | +| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | +| `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) | +| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token | +| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | +| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | +| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | +| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | +| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | +| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | +| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | +| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | +| `REDIS_HOST` | `string` | | The host where _Redis_ is running | +| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | +| `REDIS_PORT` | `number` | | The port where _Redis_ is running | +| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | +| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | #### OpenID Connect OIDC (Experimental) diff --git a/prisma/seed.mts b/prisma/seed.mts index 4819e241e..81f34d05a 100644 --- a/prisma/seed.mts +++ b/prisma/seed.mts @@ -2,7 +2,7 @@ import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; const adapter = new PrismaPg({ - connectionString: process.env.DATABASE_URL + connectionString: process.env.DIRECT_URL ?? process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); From e1e6ae7b5712619db74ee7dd00da2708357adf05 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 17 May 2026 13:52:00 +0700 Subject: [PATCH 8/8] Task/improve type safety in admin market data component (#6885) Improve type safety --- .../admin-market-data.component.ts | 152 ++++++++---------- libs/common/src/lib/config.ts | 3 + .../treemap-chart.component.stories.ts | 4 +- 3 files changed, 75 insertions(+), 84 deletions(-) diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 0abe9ea0f..72a3c337a 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -1,7 +1,8 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { + DEFAULT_COLOR_SCHEME, DEFAULT_PAGE_SIZE, - ghostfolioScraperApiSymbolPrefix + locale } from '@ghostfolio/common/config'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { @@ -26,9 +27,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, DestroyRef, + inject, OnInit, - ViewChild + viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; @@ -97,11 +100,8 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in templateUrl: './admin-market-data.html' }) export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { - @ViewChild(MatPaginator) paginator: MatPaginator; - @ViewChild(MatSort) sort: MatSort; - - public activeFilters: Filter[] = []; - public allFilters: Filter[] = [ + protected readonly adminMarketDataService = inject(AdminMarketDataService); + protected readonly allFilters: Filter[] = [ ...Object.keys(AssetSubClass) .filter((assetSubClass) => { return assetSubClass !== 'CASH'; @@ -146,37 +146,39 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { type: 'PRESET_ID' as Filter['type'] } ]; - public benchmarks: Partial[]; - public currentDataSource: DataSource; - public currentSymbol: string; - public dataSource = new MatTableDataSource(); - public defaultDateFormat: string; - public deviceType: string; - public displayedColumns: string[] = []; - public filters$ = new Subject(); - public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; - public hasPermissionForSubscription: boolean; - public info: InfoItem; - public isLoading = true; - public isUUID = isUUID; - public placeholder = ''; - public pageSize = DEFAULT_PAGE_SIZE; - public selection: SelectionModel>; - public totalItems = 0; - public user: User; - - public constructor( - public adminMarketDataService: AdminMarketDataService, - private adminService: AdminService, - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private dialog: MatDialog, - private route: ActivatedRoute, - private router: Router, - private userService: UserService - ) { + protected dataSource = new MatTableDataSource(); + protected defaultDateFormat: string; + protected readonly displayedColumns: string[] = []; + protected readonly filters$ = new Subject(); + protected isLoading = true; + protected readonly isUUID = isUUID; + protected pageSize = DEFAULT_PAGE_SIZE; + protected placeholder = ''; + protected readonly selection = new SelectionModel(true); + protected totalItems = 0; + protected user: User; + + private activeFilters: Filter[] = []; + private benchmarks: Partial[]; + private readonly deviceType = computed( + () => this.deviceDetectorService.deviceInfo().deviceType + ); + private readonly hasPermissionForSubscription: boolean; + private readonly info: InfoItem; + private readonly paginator = viewChild.required(MatPaginator); + private readonly sort = viewChild.required(MatSort); + + private readonly adminService = inject(AdminService); + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public constructor() { this.info = this.dataService.fetchInfo(); this.hasPermissionForSubscription = hasPermission( @@ -255,14 +257,14 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { } public ngAfterViewInit() { - this.sort.sortChange.subscribe( + this.sort().sortChange.subscribe( ({ active: sortColumn, direction }: Sort) => { - this.paginator.pageIndex = 0; + this.paginator().pageIndex = 0; this.loadData({ sortColumn, sortDirection: direction, - pageIndex: this.paginator.pageIndex + pageIndex: this.paginator().pageIndex }); } ); @@ -272,24 +274,24 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { const { benchmarks } = this.dataService.fetchInfo(); this.benchmarks = benchmarks; - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - - this.selection = new SelectionModel(true); } - public onChangePage(page: PageEvent) { + protected onChangePage(page: PageEvent) { this.loadData({ pageIndex: page.pageIndex, - sortColumn: this.sort.active, - sortDirection: this.sort.direction + sortColumn: this.sort().active, + sortDirection: this.sort().direction }); } - public onDeleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) { + protected onDeleteAssetProfile({ + dataSource, + symbol + }: AssetProfileIdentifier) { this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); } - public onDeleteAssetProfiles() { + protected onDeleteAssetProfiles() { this.adminMarketDataService.deleteAssetProfiles( this.selection.selected.map(({ dataSource, symbol }) => { return { dataSource, symbol }; @@ -297,7 +299,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { ); } - public onGather7Days() { + protected onGather7Days() { this.adminService .gather7Days() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -308,7 +310,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); } - public onGatherMax() { + protected onGatherMax() { this.adminService .gatherMax() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -319,31 +321,14 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); } - public onGatherProfileData() { + protected onGatherProfileData() { this.adminService .gatherProfileData() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); } - public onGatherProfileDataBySymbol({ - dataSource, - symbol - }: AssetProfileIdentifier) { - this.adminService - .gatherProfileDataBySymbol({ dataSource, symbol }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - } - - public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) { - this.adminService - .gatherSymbol({ dataSource, symbol }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - } - - public onOpenAssetProfileDialog({ + protected onOpenAssetProfileDialog({ dataSource, symbol }: AssetProfileIdentifier) { @@ -375,8 +360,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { ? Number.MAX_SAFE_INTEGER : DEFAULT_PAGE_SIZE; - if (pageIndex === 0 && this.paginator) { - this.paginator.pageIndex = 0; + if (pageIndex === 0 && this.paginator()) { + this.paginator().pageIndex = 0; } this.placeholder = @@ -406,7 +391,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }; }) ); - this.dataSource.sort = this.sort; + this.dataSource.sort = this.sort(); this.isLoading = false; @@ -435,12 +420,13 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { data: { dataSource, symbol, - colorScheme: this.user?.settings.colorScheme, - deviceType: this.deviceType, - locale: this.user?.settings?.locale - }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + colorScheme: + this.user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME, + deviceType: this.deviceType(), + locale: this.user?.settings?.locale ?? locale + } satisfies AssetProfileDialogParams, + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -471,10 +457,10 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { >(GfCreateAssetProfileDialogComponent, { autoFocus: false, data: { - deviceType: this.deviceType, - locale: this.user?.settings?.locale - }, - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + deviceType: this.deviceType(), + locale: this.user?.settings?.locale ?? locale + } satisfies CreateAssetProfileDialogParams, + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 42e1f6b63..9d6122c6c 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -2,6 +2,8 @@ import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client'; import { JobOptions, JobStatus } from 'bull'; import ms from 'ms'; +import { ColorScheme } from './types'; + export const ghostfolioPrefix = 'GF'; export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = @@ -77,6 +79,7 @@ export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW = export const STATISTICS_GATHERING_QUEUE = 'STATISTICS_GATHERING_QUEUE'; +export const DEFAULT_COLOR_SCHEME: ColorScheme = 'LIGHT'; export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_HOST = '0.0.0.0'; diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts index c8951ce6b..e98b85252 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts @@ -1,3 +1,5 @@ +import { DEFAULT_COLOR_SCHEME } from '@ghostfolio/common/config'; + import { CommonModule } from '@angular/common'; import '@angular/localize/init'; import { moduleMetadata } from '@storybook/angular'; @@ -37,7 +39,7 @@ export const Default: Story = { args: { holdings, baseCurrency: 'USD', - colorScheme: 'LIGHT', + colorScheme: DEFAULT_COLOR_SCHEME, cursor: undefined, dateRange: 'mtd', locale: 'en-US'