diff --git a/CHANGELOG.md b/CHANGELOG.md
index e25e3b1eb..eaf9b73d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +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
+## 2.203.0 - 2025-09-27
+
+### Added
+
+- Added support for column sorting to the queue jobs table in the admin control panel
+- Added a blog post: _Hacktoberfest 2025_
### Changed
diff --git a/apps/api/src/app/endpoints/sitemap/sitemap.controller.ts b/apps/api/src/app/endpoints/sitemap/sitemap.controller.ts
index fb581c72e..b42ae3594 100644
--- a/apps/api/src/app/endpoints/sitemap/sitemap.controller.ts
+++ b/apps/api/src/app/endpoints/sitemap/sitemap.controller.ts
@@ -37,6 +37,7 @@ export class SitemapController {
response.setHeader('content-type', 'application/xml');
response.send(
interpolate(this.sitemapXml, {
+ blogPosts: this.sitemapService.getBlogPosts({ currentDate }),
personalFinanceTools: this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
diff --git a/apps/api/src/app/endpoints/sitemap/sitemap.service.ts b/apps/api/src/app/endpoints/sitemap/sitemap.service.ts
index 3774d2274..359a29531 100644
--- a/apps/api/src/app/endpoints/sitemap/sitemap.service.ts
+++ b/apps/api/src/app/endpoints/sitemap/sitemap.service.ts
@@ -17,6 +17,121 @@ export class SitemapService {
private readonly i18nService: I18nService
) {}
+ public getBlogPosts({ currentDate }: { currentDate: string }) {
+ const rootUrl = this.configurationService.get('ROOT_URL');
+
+ return [
+ {
+ languageCode: 'de',
+ routerLink: ['2021', '07', 'hallo-ghostfolio']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2021', '07', 'hello-ghostfolio']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '01', 'ghostfolio-first-months-in-open-source']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '07', 'ghostfolio-meets-internet-identity']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '07', 'how-do-i-get-my-finances-in-order']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '08', '500-stars-on-github']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '10', 'hacktoberfest-2022']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2022', '11', 'black-friday-2022']
+ },
+ {
+ languageCode: 'en',
+ routerLink: [
+ '2022',
+ '12',
+ 'the-importance-of-tracking-your-personal-finances'
+ ]
+ },
+ {
+ languageCode: 'de',
+ routerLink: ['2023', '01', 'ghostfolio-auf-sackgeld-vorgestellt']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '02', 'ghostfolio-meets-umbrel']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '03', 'ghostfolio-reaches-1000-stars-on-github']
+ },
+ {
+ languageCode: 'en',
+ routerLink: [
+ '2023',
+ '05',
+ 'unlock-your-financial-potential-with-ghostfolio'
+ ]
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '07', 'exploring-the-path-to-fire']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '08', 'ghostfolio-joins-oss-friends']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '09', 'ghostfolio-2']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '09', 'hacktoberfest-2023']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '11', 'black-week-2023']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2023', '11', 'hacktoberfest-2023-debriefing']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2024', '09', 'hacktoberfest-2024']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2024', '11', 'black-weeks-2024']
+ },
+ {
+ languageCode: 'en',
+ routerLink: ['2025', '09', 'hacktoberfest-2025']
+ }
+ ]
+ .map(({ languageCode, routerLink }) => {
+ return this.createRouteSitemapUrl({
+ currentDate,
+ languageCode,
+ rootUrl,
+ route: {
+ routerLink: [publicRoutes.blog.path, ...routerLink],
+ path: undefined
+ }
+ });
+ })
+ .join('\n');
+ }
+
public getPersonalFinanceTools({ currentDate }: { currentDate: string }) {
const rootUrl = this.configurationService.get('ROOT_URL');
@@ -43,20 +158,21 @@ export class SitemapService {
});
return personalFinanceTools.map(({ alias, key }) => {
- const location = [
- rootUrl,
- languageCode,
+ const routerLink = [
resourcesPath,
personalFinanceToolsPath,
`${productPath}-${alias ?? key}`
- ].join('/');
-
- return [
- ' ',
- ` ${location} `,
- ` ${currentDate}T00:00:00+00:00 `,
- ' '
- ].join('\n');
+ ];
+
+ return this.createRouteSitemapUrl({
+ currentDate,
+ languageCode,
+ rootUrl,
+ route: {
+ routerLink,
+ path: undefined
+ }
+ });
});
}).join('\n');
}
diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml
index fb2a5403e..2d4d121bf 100644
--- a/apps/api/src/assets/sitemap.xml
+++ b/apps/api/src/assets/sitemap.xml
@@ -5,5 +5,6 @@
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${publicRoutes}
+ ${blogPosts}
${personalFinanceTools}
diff --git a/apps/api/src/middlewares/html-template.middleware.ts b/apps/api/src/middlewares/html-template.middleware.ts
index 665b93354..892b1ab5e 100644
--- a/apps/api/src/middlewares/html-template.middleware.ts
+++ b/apps/api/src/middlewares/html-template.middleware.ts
@@ -75,6 +75,10 @@ const locales = {
'/en/blog/2024/11/black-weeks-2024': {
featureGraphicPath: 'assets/images/blog/black-weeks-2024.jpg',
title: `Black Weeks 2024 - ${title}`
+ },
+ '/en/blog/2025/09/hacktoberfest-2025': {
+ featureGraphicPath: 'assets/images/blog/hacktoberfest-2025.png',
+ title: `Hacktoberfest 2025 - ${title}`
}
};
diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
index d28749b9c..8ed72445f 100644
--- a/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
+++ b/apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
@@ -16,7 +16,8 @@ import {
ChangeDetectorRef,
Component,
OnDestroy,
- OnInit
+ OnInit,
+ ViewChild
} from '@angular/core';
import {
FormBuilder,
@@ -27,6 +28,7 @@ import {
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
+import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { IonIcon } from '@ionic/angular/standalone';
import { JobStatus } from 'bull';
@@ -44,6 +46,7 @@ import {
removeCircleOutline,
timeOutline
} from 'ionicons/icons';
+import { get } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -57,6 +60,7 @@ import { takeUntil } from 'rxjs/operators';
MatButtonModule,
MatMenuModule,
MatSelectModule,
+ MatSortModule,
MatTableModule,
NgxSkeletonLoaderModule,
ReactiveFormsModule
@@ -66,6 +70,8 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './admin-jobs.html'
})
export class GfAdminJobsComponent implements OnDestroy, OnInit {
+ @ViewChild(MatSort) sort: MatSort;
+
public DATA_GATHERING_QUEUE_PRIORITY_LOW = DATA_GATHERING_QUEUE_PRIORITY_LOW;
public DATA_GATHERING_QUEUE_PRIORITY_HIGH =
DATA_GATHERING_QUEUE_PRIORITY_HIGH;
@@ -196,6 +202,8 @@ export class GfAdminJobsComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ jobs }) => {
this.dataSource = new MatTableDataSource(jobs);
+ this.dataSource.sort = this.sort;
+ this.dataSource.sortingDataAccessor = get;
this.isLoading = false;
diff --git a/apps/client/src/app/components/admin-jobs/admin-jobs.html b/apps/client/src/app/components/admin-jobs/admin-jobs.html
index f2bfaa931..14f1b211b 100644
--- a/apps/client/src/app/components/admin-jobs/admin-jobs.html
+++ b/apps/client/src/app/components/admin-jobs/admin-jobs.html
@@ -16,9 +16,21 @@
-
+
-
+
Job ID
@@ -27,7 +39,12 @@
-
+
Type
@@ -42,7 +59,12 @@
-
+
Symbol
@@ -51,7 +73,12 @@
-
+
Data Source
@@ -60,7 +87,12 @@
-
+
Priority
@@ -79,7 +111,12 @@
-
+
Attempts
@@ -88,7 +125,12 @@
-
+
Created
diff --git a/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts
new file mode 100644
index 000000000..72990ca47
--- /dev/null
+++ b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component.ts
@@ -0,0 +1,17 @@
+import { publicRoutes } from '@ghostfolio/common/routes/routes';
+
+import { Component } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterModule } from '@angular/router';
+
+@Component({
+ host: { class: 'page' },
+ imports: [MatButtonModule, RouterModule],
+ selector: 'gf-hacktoberfest-2025-page',
+ templateUrl: './hacktoberfest-2025-page.html'
+})
+export class Hacktoberfest2025PageComponent {
+ public routerLinkAbout = publicRoutes.about.routerLink;
+ public routerLinkBlog = publicRoutes.blog.routerLink;
+ public routerLinkOpenStartup = publicRoutes.openStartup.routerLink;
+}
diff --git a/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.html b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.html
new file mode 100644
index 000000000..bde5a2fee
--- /dev/null
+++ b/apps/client/src/app/pages/blog/2025/09/hacktoberfest-2025/hacktoberfest-2025-page.html
@@ -0,0 +1,201 @@
+
+
+
+
+
+
Hacktoberfest 2025
+
2025-09-27
+
+
+
+
+ Ghostfolio is joining
+ Hacktoberfest for the fourth
+ time and we are looking
+ forward to meeting new open-source contributors along the way. Every
+ year in October, Hacktoberfest celebrates open source by
+ highlighting projects, maintainers, and contributors from around the
+ globe. Open source maintainers dedicate extra time to support new
+ contributors while guiding them through their first pull requests on
+ GitHub .
+
+
+
+
+ Meet Ghostfolio: a modern Dashboard for Personal Finance
+
+
+ Ghostfolio is a web application
+ that makes it easy to manage your personal finances. It aggregates
+ your assets and helps you make informed decisions to balance your
+ portfolio or plan future investments.
+
+
+ The software is fully written in
+ TypeScript and
+ organized as an Nx workspace, utilizing
+ the latest framework releases. The backend is based on
+ NestJS in combination with
+ PostgreSQL as a database
+ together with Prisma and
+ Redis for caching. The frontend is
+ developed with Angular .
+
+
+ With over 200 contributors, the OSS project is used daily by a
+ growing global community. Ghostfolio counts more than
+ 6’500 stars on GitHub
+ and
+ 1’600’000+ pulls on Docker Hub , standing out for its simple and user-friendly experience.
+
+
+
+ How you can make an impact
+
+ Every contribution makes a difference. Whether it is implementing
+ new features, resolving bugs, refactoring code, enhancing
+ documentation, adding unit tests, or translating content into
+ another language, you can actively shape our project.
+
+
+ New to our codebase? No worries! We have labeled a few
+ issues
+ with hacktoberfest that are ideal for newcomers.
+
+
+ The official Hacktoberfest website provides some valuable
+ resources for beginners
+ to start contributing in open source.
+
+
+
+ Connect with us
+
+ If you have further questions or ideas, please join our
+ Slack
+ community or get in touch on X
+ @ghostfolio_ .
+
+
+ We look forward to collaborating.
+ Thomas from Ghostfolio
+
+
+
+
+
+ Angular
+
+
+ Community
+
+
+ Dashboard
+
+
+ Docker
+
+
+ Finance
+
+
+ Fintech
+
+
+ Ghostfolio
+
+
+ GitHub
+
+
+ Hacktoberfest
+
+
+ Hacktoberfest 2025
+
+
+ Investment
+
+
+ NestJS
+
+
+ Nx
+
+
+ October
+
+
+ Open Source
+
+
+ OSS
+
+
+ Personal Finance
+
+
+ Portfolio
+
+
+ Portfolio Tracker
+
+
+ Prisma
+
+
+ Redis
+
+
+ Software
+
+
+ TypeScript
+
+
+ UX
+
+
+ Wealth
+
+
+ Wealth Management
+
+
+ Web Application
+
+
+
+
+
+
+ Blog
+
+
+ Hacktoberfest 2025
+
+
+
+
+
+
+
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 0e00ee530..9b352b7a8 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
@@ -201,6 +201,15 @@ const routes: Routes = [
(c) => c.BlackWeeks2024PageComponent
),
title: 'Black Weeks 2024'
+ },
+ {
+ canActivate: [AuthGuard],
+ path: '2025/09/hacktoberfest-2025',
+ loadComponent: () =>
+ import(
+ './2025/09/hacktoberfest-2025/hacktoberfest-2025-page.component'
+ ).then((c) => c.Hacktoberfest2025PageComponent),
+ title: 'Hacktoberfest 2025'
}
];
diff --git a/apps/client/src/app/pages/blog/blog-page.html b/apps/client/src/app/pages/blog/blog-page.html
index babeec4c6..88b685d33 100644
--- a/apps/client/src/app/pages/blog/blog-page.html
+++ b/apps/client/src/app/pages/blog/blog-page.html
@@ -8,6 +8,30 @@
finance
+
+
+
+
+
@if (hasPermissionForSubscription) {
diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
index 33cf5148b..a722dffbf 100644
--- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
@@ -305,10 +305,10 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
});
}
- public openUpdateActivityDialog(activity: Activity) {
+ public openUpdateActivityDialog(aActivity: Activity) {
const dialogRef = this.dialog.open(GfCreateOrUpdateActivityDialog, {
data: {
- activity,
+ activity: aActivity,
accounts: this.user?.accounts,
user: this.user
},
@@ -319,10 +319,10 @@ export class GfActivitiesPageComponent implements OnDestroy, OnInit {
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((transaction: UpdateOrderDto | null) => {
- if (transaction) {
+ .subscribe((activity: UpdateOrderDto) => {
+ if (activity) {
this.dataService
- .putOrder(transaction)
+ .putOrder(activity)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index 4675b2388..ef89094b9 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -518,48 +518,6 @@ export class DataService {
);
}
- public fetchSymbolItem({
- dataSource,
- includeHistoricalData,
- symbol
- }: {
- dataSource: DataSource | string;
- includeHistoricalData?: number;
- symbol: string;
- }) {
- let params = new HttpParams();
-
- if (includeHistoricalData) {
- params = params.append('includeHistoricalData', includeHistoricalData);
- }
-
- return this.http.get(`/api/v1/symbol/${dataSource}/${symbol}`, {
- params
- });
- }
-
- public fetchSymbols({
- includeIndices = false,
- query
- }: {
- includeIndices?: boolean;
- query: string;
- }) {
- let params = new HttpParams().set('query', query);
-
- if (includeIndices) {
- params = params.append('includeIndices', includeIndices);
- }
-
- return this.http
- .get('/api/v1/symbol/lookup', { params })
- .pipe(
- map(({ items }) => {
- return items;
- })
- );
- }
-
public fetchPortfolioDetails({
filters,
withMarkets = false
@@ -731,6 +689,48 @@ export class DataService {
);
}
+ public fetchSymbolItem({
+ dataSource,
+ includeHistoricalData,
+ symbol
+ }: {
+ dataSource: DataSource | string;
+ includeHistoricalData?: number;
+ symbol: string;
+ }) {
+ let params = new HttpParams();
+
+ if (includeHistoricalData) {
+ params = params.append('includeHistoricalData', includeHistoricalData);
+ }
+
+ return this.http.get(`/api/v1/symbol/${dataSource}/${symbol}`, {
+ params
+ });
+ }
+
+ public fetchSymbols({
+ includeIndices = false,
+ query
+ }: {
+ includeIndices?: boolean;
+ query: string;
+ }) {
+ let params = new HttpParams().set('query', query);
+
+ if (includeIndices) {
+ params = params.append('includeIndices', includeIndices);
+ }
+
+ return this.http
+ .get('/api/v1/symbol/lookup', { params })
+ .pipe(
+ map(({ items }) => {
+ return items;
+ })
+ );
+ }
+
public fetchTags() {
return this.http.get('/api/v1/tags');
}
diff --git a/apps/client/src/assets/images/blog/hacktoberfest-2025.png b/apps/client/src/assets/images/blog/hacktoberfest-2025.png
new file mode 100644
index 000000000..ab04e6dce
Binary files /dev/null and b/apps/client/src/assets/images/blog/hacktoberfest-2025.png differ
diff --git a/package-lock.json b/package-lock.json
index 7b31242c0..2e5af6455 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "2.202.0",
+ "version": "2.203.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "2.202.0",
+ "version": "2.203.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -130,7 +130,7 @@
"@types/big.js": "6.2.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13",
- "@types/lodash": "4.17.17",
+ "@types/lodash": "4.17.20",
"@types/node": "22.15.17",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16",
@@ -14435,9 +14435,9 @@
}
},
"node_modules/@types/lodash": {
- "version": "4.17.17",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",
- "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index 463310c60..8f2a908c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "2.202.0",
+ "version": "2.203.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@@ -176,7 +176,7 @@
"@types/big.js": "6.2.2",
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13",
- "@types/lodash": "4.17.17",
+ "@types/lodash": "4.17.20",
"@types/node": "22.15.17",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16",