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
+ Hacktoberfest 2025 with Ghostfolio Teaser +
+
+

+ 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 +
  • +
+
+ +
+
+
+
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",