diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b1f784b2..776872a21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Extended the user account settings with a copy-to-clipboard button for the user id
+- Added pagination to the platform management of the admin control panel
+- Added pagination to the tag management of the admin control panel
+
+### Fixed
+
+- Fixed an issue with hourly market data updates not refreshing prices for asset profiles with `MANUAL` data source
## 3.15.1 - 2026-06-23
diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts
index 5d49848fa..10b0e6fd8 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -662,7 +662,11 @@ export class DataProviderService implements OnModuleInit {
);
const promise = Promise.resolve(
- dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
+ dataProvider.getQuotes({
+ requestTimeout,
+ useCache,
+ symbols: symbolsChunk
+ })
);
promises.push(
diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
index a55c9f328..5002fa87e 100644
--- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
+++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
@@ -75,6 +75,7 @@ export interface GetHistoricalParams {
export interface GetQuotesParams {
requestTimeout?: number;
symbols: string[];
+ useCache?: boolean;
}
export interface GetSearchParams {
diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts
index 87e116dda..66571c239 100644
--- a/apps/api/src/services/data-provider/manual/manual.service.ts
+++ b/apps/api/src/services/data-provider/manual/manual.service.ts
@@ -136,7 +136,8 @@ export class ManualService implements DataProviderInterface {
}
public async getQuotes({
- symbols
+ symbols,
+ useCache = true
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
@@ -164,32 +165,32 @@ export class ManualService implements DataProviderInterface {
}
});
- const symbolProfilesWithScraperConfigurationAndInstantMode =
- symbolProfiles.filter(({ scraperConfiguration }) => {
+ const symbolProfilesToScrape = symbolProfiles.filter(
+ ({ scraperConfiguration }) => {
return (
- scraperConfiguration?.mode === 'instant' &&
+ (scraperConfiguration?.mode === 'instant' || !useCache) &&
scraperConfiguration?.selector &&
scraperConfiguration?.url
);
- });
-
- const scraperResultPromises =
- symbolProfilesWithScraperConfigurationAndInstantMode.map(
- async ({ scraperConfiguration, symbol }) => {
- try {
- const marketPrice = await this.scrape({
- scraperConfiguration,
- symbol
- });
- return { marketPrice, symbol };
- } catch (error) {
- this.logger.error(
- `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
- );
- return { symbol, marketPrice: undefined };
- }
+ }
+ );
+
+ const scraperResultPromises = symbolProfilesToScrape.map(
+ async ({ scraperConfiguration, symbol }) => {
+ try {
+ const marketPrice = await this.scrape({
+ scraperConfiguration,
+ symbol
+ });
+ return { marketPrice, symbol };
+ } catch (error) {
+ this.logger.error(
+ `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
+ );
+ return { symbol, marketPrice: undefined };
}
- );
+ }
+ );
// Wait for all scraping requests to complete concurrently
const scraperResults = await Promise.all(scraperResultPromises);
diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.html b/apps/client/src/app/components/admin-platform/admin-platform.component.html
index 44f5a6eab..19682bdc0 100644
--- a/apps/client/src/app/components/admin-platform/admin-platform.component.html
+++ b/apps/client/src/app/components/admin-platform/admin-platform.component.html
@@ -96,3 +96,9 @@
+
+
diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
index 26e6c2b1e..727585478 100644
--- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts
+++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
@@ -1,4 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
@@ -22,6 +23,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@@ -46,6 +48,7 @@ import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-
IonIcon,
MatButtonModule,
MatMenuModule,
+ MatPaginatorModule,
MatSortModule,
MatTableModule,
RouterModule
@@ -59,11 +62,13 @@ export class GfAdminPlatformComponent implements OnInit {
protected dataSource = new MatTableDataSource();
protected readonly displayedColumns = ['name', 'url', 'accounts', 'actions'];
+ protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected platforms: Platform[];
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
+ private readonly paginator = viewChild.required(MatPaginator);
private readonly sort = viewChild.required(MatSort);
private readonly adminService = inject(AdminService);
@@ -145,6 +150,7 @@ export class GfAdminPlatformComponent implements OnInit {
this.platforms = platforms;
this.dataSource = new MatTableDataSource(platforms);
+ this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = getLowercase;
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.scss b/apps/client/src/app/components/admin-settings/admin-settings.component.scss
index 1c0a17624..29340ea0d 100644
--- a/apps/client/src/app/components/admin-settings/admin-settings.component.scss
+++ b/apps/client/src/app/components/admin-settings/admin-settings.component.scss
@@ -10,7 +10,33 @@
}
.mat-mdc-card {
- --mat-card-outlined-container-color: whitesmoke;
+ --gradient-opacity: 50%;
+ --mat-card-outlined-outline-color: rgb(193, 219, 254);
+
+ background-color: white;
+ background-image: linear-gradient(
+ 109.6deg,
+ color-mix(in srgb, rgba(255, 255, 255, 1) 100%, transparent) 11.2%,
+ color-mix(
+ in srgb,
+ rgba(221, 108, 241, 0.26) var(--gradient-opacity),
+ transparent
+ )
+ 42%,
+ color-mix(
+ in srgb,
+ rgba(229, 106, 253, 0.71) var(--gradient-opacity),
+ transparent
+ )
+ 71.5%,
+ color-mix(
+ in srgb,
+ rgba(123, 183, 253, 1) var(--gradient-opacity),
+ transparent
+ )
+ 100%
+ );
+ color: rgb(var(--dark-primary-text));
.mat-mdc-card-actions {
min-height: 0;
@@ -32,6 +58,6 @@
:host-context(.theme-dark) {
.mat-mdc-card {
- --mat-card-outlined-container-color: #222222;
+ --mat-card-outlined-outline-color: white;
}
}
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html
index 3c125d5c0..a84cbb283 100644
--- a/apps/client/src/app/components/admin-tag/admin-tag.component.html
+++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html
@@ -89,3 +89,9 @@
+
+
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
index fcb901707..7bd69c964 100644
--- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts
+++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
@@ -1,4 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
@@ -21,6 +22,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@@ -44,6 +46,7 @@ import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/int
IonIcon,
MatButtonModule,
MatMenuModule,
+ MatPaginatorModule,
MatSortModule,
MatTableModule,
RouterModule
@@ -62,11 +65,13 @@ export class GfAdminTagComponent implements OnInit {
'activities',
'actions'
];
+ protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected tags: Tag[];
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
+ private readonly paginator = viewChild.required(MatPaginator);
private readonly sort = viewChild.required(MatSort);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
@@ -147,6 +152,7 @@ export class GfAdminTagComponent implements OnInit {
this.tags = tags;
this.dataSource = new MatTableDataSource(this.tags);
+ this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = getLowercase;
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index f96a934e7..03600088f 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -90,10 +90,12 @@ export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1;
-export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT = 60000;
+export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT =
+ ms('1 minute');
export const DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1;
-export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
+export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT =
+ ms('30 seconds');
export const DEFAULT_REDACTED_PATHS = [
'accounts[*].balance',