From fd2df8de573b73bac96e1e4cd4f34b2468da8ffe Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 4 Oct 2026 13:27:08 +0200
Subject: [PATCH 1/3] Task/move dividend and dividend yield in holding detail
dialog from experimental to general availability (#8047)
* Move dividend and dividend yield from experimental to general availability
* Update changelog
---
CHANGELOG.md | 1 +
.../holding-detail-dialog/holding-detail-dialog.html | 4 +---
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dff0d90ded..614104b8f8 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
### Changed
- Localized the number formatting in the chart of the holdings tab on the home page
+- Moved the dividend and the dividend yield in the holding detail dialog from experimental to general availability
- Changed the installation of the dependencies from `npm install` to `npm ci` in the `Dockerfile`
- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.3` to `14.0`
- Upgraded `nestjs` from version `11.2.3` to `11.2.6`
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 2b378a4d60..b54cc68f7d 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
@@ -183,9 +183,7 @@
>Invested Capital
- @if (
- dividendInBaseCurrency && user?.settings?.isExperimentalFeatures
- ) {
+ @if (dividendInBaseCurrency) {
Date: Sun, 4 Oct 2026 13:27:36 +0200
Subject: [PATCH 2/3] Task/setup test for route guards (#8044)
Setup test for route guards
---
apps/api/src/app/app.module.spec.ts | 172 ++++++++++++++++++++++++++++
1 file changed, 172 insertions(+)
create mode 100644 apps/api/src/app/app.module.spec.ts
diff --git a/apps/api/src/app/app.module.spec.ts b/apps/api/src/app/app.module.spec.ts
new file mode 100644
index 0000000000..976e08393b
--- /dev/null
+++ b/apps/api/src/app/app.module.spec.ts
@@ -0,0 +1,172 @@
+import {
+ DynamicModule,
+ ForwardReference,
+ RequestMethod,
+ Type
+} from '@nestjs/common';
+import {
+ GUARDS_METADATA,
+ METHOD_METADATA,
+ MODULE_METADATA,
+ PATH_METADATA
+} from '@nestjs/common/constants';
+import { MetadataScanner } from '@nestjs/core';
+import { AuthGuard } from '@nestjs/passport';
+
+import { AppModule } from './app.module';
+
+// The packages are ECMAScript modules, which Jest cannot load
+jest.mock('@openrouter/ai-sdk-provider', () => {
+ return {};
+});
+jest.mock('ai', () => {
+ return {};
+});
+
+type ModuleDefinition =
+ DynamicModule | ForwardReference<() => Type> | Promise | Type;
+
+/**
+ * The routes of the controllers which answer a request without the
+ * authentication of a user or of an API key, sorted by the request method and
+ * the path. A new route has to apply AuthGuard('api-key') or AuthGuard('jwt')
+ * (e.g. via the decorator RequiresScope) or has to be added here.
+ *
+ * The MCP transport, Bull Board and the static files are not covered, as they
+ * are not served by a controller
+ */
+const PUBLIC_ROUTES = [
+ 'GET /asset/:dataSource/:symbol',
+ 'GET /assets/:languageCode/site.webmanifest',
+ 'GET /auth/google',
+ 'GET /auth/google/callback',
+ 'GET /auth/oidc',
+ 'GET /auth/oidc/callback',
+ 'GET /benchmarks',
+ 'GET /health',
+ 'GET /health/ai',
+ 'GET /health/data-enhancer/:name',
+ 'GET /health/data-provider/:dataSource',
+ 'GET /health/liveness',
+ 'GET /info',
+ 'GET /logo',
+ 'GET /logo/:dataSource/:symbol',
+ 'GET /public/:accessId/portfolio',
+ 'GET /sitemap.xml',
+ 'GET /subscription/stripe/callback',
+ 'POST /auth/anonymous',
+ 'POST /auth/webauthn/generate-authentication-options',
+ 'POST /auth/webauthn/verify-authentication',
+ 'POST /user'
+];
+
+/**
+ * Gives the controllers of the module and of each module which it imports
+ */
+async function getControllers(
+ moduleDefinition: ModuleDefinition,
+ visitedModules = new Set()
+): Promise {
+ const resolvedModule = await ('forwardRef' in moduleDefinition
+ ? moduleDefinition.forwardRef()
+ : moduleDefinition);
+
+ if (visitedModules.has(resolvedModule)) {
+ return [];
+ }
+
+ visitedModules.add(resolvedModule);
+
+ const isDynamicModule = 'module' in resolvedModule;
+
+ const controllers = [
+ ...(isDynamicModule
+ ? (resolvedModule.controllers ?? [])
+ : ((Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, resolvedModule) ??
+ []) as Type[]))
+ ];
+
+ const importedModules = isDynamicModule
+ ? [resolvedModule.module, ...(resolvedModule.imports ?? [])]
+ : ((Reflect.getMetadata(MODULE_METADATA.IMPORTS, resolvedModule) ??
+ []) as ModuleDefinition[]);
+
+ for (const importedModule of importedModules) {
+ controllers.push(...(await getControllers(importedModule, visitedModules)));
+ }
+
+ return [...new Set(controllers)];
+}
+
+/**
+ * Gives the paths which the decorator of a controller or of a route sets
+ */
+function getPaths(target: object) {
+ return [
+ (Reflect.getMetadata(PATH_METADATA, target) ?? '') as string | string[]
+ ].flat();
+}
+
+/**
+ * Gives the routes of the controller which apply neither AuthGuard('api-key')
+ * nor AuthGuard('jwt'), each as the request method and the path
+ */
+function getRoutesWithoutAuthentication(controller: Type) {
+ const authenticationGuards: unknown[] = [
+ AuthGuard('api-key'),
+ AuthGuard('jwt')
+ ];
+ const routeHandlersByName = controller.prototype as Record;
+
+ return new MetadataScanner()
+ .getAllMethodNames(routeHandlersByName)
+ .flatMap((methodName) => {
+ const routeHandler = routeHandlersByName[methodName];
+ const requestMethod = Reflect.getMetadata(
+ METHOD_METADATA,
+ routeHandler
+ ) as RequestMethod | undefined;
+
+ if (requestMethod === undefined) {
+ return [];
+ }
+
+ const guards = [controller, routeHandler].flatMap((target) => {
+ return (Reflect.getMetadata(GUARDS_METADATA, target) ??
+ []) as unknown[];
+ });
+
+ const hasAuthentication = guards.some((guard) => {
+ return authenticationGuards.includes(guard);
+ });
+
+ if (hasAuthentication) {
+ return [];
+ }
+
+ return getPaths(controller).flatMap((controllerPath) => {
+ return getPaths(routeHandler).map((routePath) => {
+ const path = `${controllerPath}/${routePath}`
+ .split('/')
+ .filter(Boolean)
+ .join('/');
+
+ return `${RequestMethod[requestMethod]} /${path}`;
+ });
+ });
+ });
+}
+
+describe('AppModule', () => {
+ it('should require the authentication for each route which is not public', async () => {
+ const controllers = await getControllers(AppModule);
+
+ const routesWithoutAuthentication = controllers
+ .flatMap((controller) => {
+ return getRoutesWithoutAuthentication(controller);
+ })
+ .sort();
+
+ expect(routesWithoutAuthentication).toEqual(PUBLIC_ROUTES);
+ });
+});
From 4200af750fa2ac200a7ac6f629866e59a23c13ab Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 4 Oct 2026 13:30:45 +0200
Subject: [PATCH 3/3] Release 3.79.0 (#8048)
---
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 614104b8f8..245fadf2de 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
+## 3.79.0 - 2026-10-04
### Added
diff --git a/package-lock.json b/package-lock.json
index b25373e7b4..57eb3b25a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "3.78.0",
+ "version": "3.79.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "3.78.0",
+ "version": "3.79.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
diff --git a/package.json b/package.json
index d4d7169c0d..2e7b433487 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "3.78.0",
+ "version": "3.79.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",