diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9688c4ac42..e5c182608e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+- Added a _Storybook_ story for the investment chart component
+
### Changed
+- Moved the investment chart component to `@ghostfolio/ui`
- Improved the matching of assets in the import of activities with _Ghostfolio AI_ to prefer the holdings of the user (experimental)
- Extended the holdings table by the ISIN in the _Copy AI prompt to clipboard_ actions on the analysis page and in the tool to get the portfolio of the server of the Model Context Protocol (MCP) (experimental)
- Hardened the validation of the date in the endpoint to create an account balance
@@ -18,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed an exception in the authentication interceptor when the time zone of the browser is not available
+- Fixed the missing `userId` in the unique constraint of the `AccountBalance` database schema
## 3.86.0 - 2026-10-13
diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts
index 56975cdf08..afa4386e01 100644
--- a/apps/api/src/app/account-balance/account-balance.service.ts
+++ b/apps/api/src/app/account-balance/account-balance.service.ts
@@ -68,9 +68,9 @@ export class AccountBalanceService {
value: balance
},
where: {
- userId,
- accountId_date: {
+ accountId_userId_date: {
accountId,
+ userId,
date: resetHours(parseISO(date))
}
}
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
index 9a1dfe9438..f6fe52a8bd 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
@@ -1,4 +1,3 @@
-import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
DEFAULT_DATE_RANGE,
@@ -23,6 +22,7 @@ import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { translate } from '@ghostfolio/ui/i18n';
+import { GfInvestmentChartComponent } from '@ghostfolio/ui/investment-chart';
import { DataService } from '@ghostfolio/ui/services';
import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
diff --git a/apps/client/src/app/components/user-account-membership/user-account-membership.html b/apps/client/src/app/components/user-account-membership/user-account-membership.html
index ae23dc9860..f18da13ed9 100644
--- a/apps/client/src/app/components/user-account-membership/user-account-membership.html
+++ b/apps/client/src/app/components/user-account-membership/user-account-membership.html
@@ -50,7 +50,7 @@
}
}
- @if (!user?.subscription?.expiresAt) {
+ @if (!user?.subscription?.subscribedAt) {
{
user.settings.retirementDate = parseISO(user.settings.retirementDate);
}
+ if (user.subscription?.expiresAt) {
+ user.subscription.expiresAt = parseISO(user.subscription.expiresAt);
+ }
+
+ if (user.subscription?.subscribedAt) {
+ user.subscription.subscribedAt = parseISO(
+ user.subscription.subscribedAt
+ );
+ }
+
this.setState({ user }, UserStoreActions.GetUser);
if (
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index 48f69fd403..615ed09214 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -31,13 +31,6 @@ export const secondaryColorRgb = {
b: 207
};
-export const warnColorHex = '#dc3545';
-export const warnColorRgb = {
- r: 220,
- g: 53,
- b: 69
-};
-
export const AI_ACTIVITIES_FILE_MAXIMUM_SIZE = 10 * 1024 * 1024;
export const AI_ACTIVITIES_TIMEOUT = ms('2 minutes');
export const AI_MAX_ACTIVITIES = 25;
diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts
index ff0f6646f5..aa079c1219 100644
--- a/libs/common/src/lib/interfaces/user.interface.ts
+++ b/libs/common/src/lib/interfaces/user.interface.ts
@@ -27,6 +27,7 @@ export interface User {
subscription: {
expiresAt?: Date;
offer: SubscriptionOffer;
+ subscribedAt?: Date;
type: SubscriptionType;
};
tags: (Tag & { isUsed: boolean })[];
diff --git a/libs/ui/src/lib/investment-chart/index.ts b/libs/ui/src/lib/investment-chart/index.ts
new file mode 100644
index 0000000000..3929e41867
--- /dev/null
+++ b/libs/ui/src/lib/investment-chart/index.ts
@@ -0,0 +1 @@
+export * from './investment-chart.component';
diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.html b/libs/ui/src/lib/investment-chart/investment-chart.component.html
similarity index 100%
rename from apps/client/src/app/components/investment-chart/investment-chart.component.html
rename to libs/ui/src/lib/investment-chart/investment-chart.component.html
diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.scss b/libs/ui/src/lib/investment-chart/investment-chart.component.scss
similarity index 100%
rename from apps/client/src/app/components/investment-chart/investment-chart.component.scss
rename to libs/ui/src/lib/investment-chart/investment-chart.component.scss
diff --git a/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts b/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts
new file mode 100644
index 0000000000..225857c7da
--- /dev/null
+++ b/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts
@@ -0,0 +1,75 @@
+import { moduleMetadata } from '@storybook/angular';
+import type { Meta, StoryObj } from '@storybook/angular';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+
+import { GfInvestmentChartComponent } from './investment-chart.component';
+
+export default {
+ title: 'Investment Chart',
+ component: GfInvestmentChartComponent,
+ decorators: [
+ moduleMetadata({
+ imports: [NgxSkeletonLoaderModule]
+ })
+ ]
+} as Meta;
+
+type Story = StoryObj;
+
+export const PerformanceByYear: Story = {
+ args: {
+ benchmarkDataItems: [
+ {
+ date: '2015-01-01',
+ investment: -0.02783300898051616
+ },
+ {
+ date: '2016-01-01',
+ investment: 0.2955008663975502
+ },
+ {
+ date: '2017-01-01',
+ investment: 0.5581973009289353
+ },
+ {
+ date: '2018-01-01',
+ investment: -0.27056776062551113
+ },
+ {
+ date: '2019-01-01',
+ investment: 0.27802802888773426
+ },
+ {
+ date: '2020-01-01',
+ investment: 0.11264960377555408
+ },
+ {
+ date: '2021-01-01',
+ investment: 0.31365756351078433
+ },
+ {
+ date: '2022-01-01',
+ investment: -0.23865494728984496
+ },
+ {
+ date: '2023-01-01',
+ investment: 0.07752911954819477
+ },
+ {
+ date: '2024-01-01',
+ investment: 0.24854314905668837
+ },
+ {
+ date: '2025-01-01',
+ investment: 0.05351048565996891
+ },
+ {
+ date: '2026-01-01',
+ investment: 0.1037188655512382
+ }
+ ],
+ benchmarkDataLabel: 'Net Performance',
+ groupBy: 'year',
+ isInPercentage: true
+ }
+};
diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/libs/ui/src/lib/investment-chart/investment-chart.component.ts
similarity index 99%
rename from apps/client/src/app/components/investment-chart/investment-chart.component.ts
rename to libs/ui/src/lib/investment-chart/investment-chart.component.ts
index 8b74a74d4c..86af956d45 100644
--- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts
+++ b/libs/ui/src/lib/investment-chart/investment-chart.component.ts
@@ -12,11 +12,6 @@ import { getLocale, parseDate } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
-import {
- getTimeSeriesTooltipOptions,
- onPrefersColorSchemeChange,
- registerChartConfiguration
-} from '@ghostfolio/ui/chart';
import {
ChangeDetectionStrategy,
@@ -47,6 +42,12 @@ import { type AnnotationOptions } from 'chartjs-plugin-annotation';
import { isFuture } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import {
+ getTimeSeriesTooltipOptions,
+ onPrefersColorSchemeChange,
+ registerChartConfiguration
+} from '../chart';
+
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgxSkeletonLoaderModule],
diff --git a/prisma/migrations/20261014000000_added_user_id_to_unique_constraint_of_account_balance/migration.sql b/prisma/migrations/20261014000000_added_user_id_to_unique_constraint_of_account_balance/migration.sql
new file mode 100644
index 0000000000..40c76e5816
--- /dev/null
+++ b/prisma/migrations/20261014000000_added_user_id_to_unique_constraint_of_account_balance/migration.sql
@@ -0,0 +1,5 @@
+-- DropIndex
+DROP INDEX "AccountBalance_accountId_date_key";
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AccountBalance_accountId_userId_date_key" ON "AccountBalance"("accountId", "userId", "date");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e21fc10e8c..17a2d4e39d 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -65,7 +65,7 @@ model AccountBalance {
userId String
value Float
- @@unique([accountId, date])
+ @@unique([accountId, userId, date])
@@index([accountId])
@@index([date])
}