From b39a806f181c0b97e43f2ee2960a730390fb1644 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:21:31 +0200 Subject: [PATCH 01/15] Task/extend personal finance tools (20260902) (#7793) Extend personal finance tools --- libs/common/src/lib/personal-finance-tools.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts index 1339ea907..d0d7a92ba 100644 --- a/libs/common/src/lib/personal-finance-tools.ts +++ b/libs/common/src/lib/personal-finance-tools.ts @@ -1590,6 +1590,26 @@ export const personalFinanceTools: Product[] = [ slogan: 'The Intelligent Portfolio Tracker', url: 'https://www.navexa.com' }, + { + categories: [ + 'BUDGETING', + 'CRYPTOCURRENCY', + 'ETF_TRACKING', + 'FINANCIAL_PLANNING', + 'NET_WORTH_TRACKING', + 'STOCK_TRACKING' + ], + hasFreePlan: true, + hasSelfHostingAbility: false, + key: 'neontra', + languages: ['English'], + name: 'Neontra', + origin: 'CA', + platforms: ['ANDROID', 'IOS', 'WEB'], + pricingPerYear: '$75', + slogan: 'Everything you own. Everything you owe. Everything in one place.', + url: 'https://neontra.com' + }, { categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'], hasFreePlan: true, From ac7c6a55c50c58e2b78e6077fa05d793e3a934be Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:21:54 +0200 Subject: [PATCH 02/15] Task/upgrade prisma to version 7.10.0 (#7790) * Update prisma to version 7.10.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 108 +++++++++++++++++++++++----------------------- package.json | 8 ++-- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b2f2e35..2f3727101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) +- Upgraded `prisma` from version `7.9.1` to `7.10.0` ### Fixed diff --git a/package-lock.json b/package-lock.json index 16df6c6b8..6d3a14071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,8 +47,8 @@ "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "3.0.0", - "@prisma/adapter-pg": "7.9.1", - "@prisma/client": "7.9.1", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.0", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", @@ -134,7 +134,7 @@ "@nx/storybook": "23.1.1", "@nx/web": "23.1.1", "@nx/workspace": "23.1.1", - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@schematics/angular": "22.1.4", "@storybook/addon-docs": "10.5.7", "@storybook/addon-themes": "10.5.7", @@ -165,7 +165,7 @@ "nx": "23.1.1", "prettier": "3.9.6", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.9.1", + "prisma": "7.10.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "9.0.0", @@ -13186,24 +13186,24 @@ } }, "node_modules/@prisma/adapter-pg": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", - "integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.0.tgz", + "integrity": "sha512-N7nwSor0HO1Kz6xBv0TPAjAPysKK0fac6p4fVN3ensLOuzc/83Fgmln5k92eK/cvzqdkSR/2kkAqlbcdwVrwpw==", "license": "Apache-2.0", "dependencies": { - "@prisma/driver-adapter-utils": "7.9.1", + "@prisma/driver-adapter-utils": "7.10.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "node_modules/@prisma/client": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.9.1.tgz", - "integrity": "sha512-+xgrh2EhJVF79wC0yX5G4PI1Rdcm7Qn/nekNQ+t/O153wtNggruHal+fXHSa0QE+Tp/Cw5wvxeCEhZZ59xGm8Q==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.0.tgz", + "integrity": "sha512-Ubw/QS9JGIBSBUsyxAUQuK/Jcu0Tsva7le7QbLd91Kix9yJvYDdj5QkwgEbbZniH80dd+sziQcALPc+HnvQC8Q==", "license": "Apache-2.0", "dependencies": { - "@prisma/client-runtime-utils": "7.9.1" + "@prisma/client-runtime-utils": "7.10.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0" @@ -13222,15 +13222,15 @@ } }, "node_modules/@prisma/client-runtime-utils": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.9.1.tgz", - "integrity": "sha512-mVIBGYdO5CFmK0HvjxrtfIyQQcPdb88pSCeVQriVQPVZyDovIWblpHfOgcS8QO187j3QF0ePArH8qPhp0AU2vg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.0.tgz", + "integrity": "sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==", "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.1.tgz", - "integrity": "sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -13241,9 +13241,9 @@ } }, "node_modules/@prisma/debug": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", - "integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.0.tgz", + "integrity": "sha512-caygJKtltmRIgdJ3jRpkOr7yM4DW6zxo5uOmojKWFb3asnxWoRkQOwZmXBgD8FZp4htrX+nMpcWqDwzlQ1+Y4g==", "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -13271,65 +13271,65 @@ } }, "node_modules/@prisma/driver-adapter-utils": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", - "integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.0.tgz", + "integrity": "sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/engines": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", - "integrity": "sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", + "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1", - "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "@prisma/fetch-engine": "7.9.1", - "@prisma/get-platform": "7.9.1" + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/fetch-engine": "7.10.0", + "@prisma/get-platform": "7.10.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz", - "integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==", + "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", + "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", - "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.1.tgz", - "integrity": "sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", + "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1", - "@prisma/engines-version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", - "@prisma/get-platform": "7.9.1" + "@prisma/debug": "7.10.0", + "@prisma/engines-version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", + "@prisma/get-platform": "7.10.0" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", - "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", + "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.9.1" + "@prisma/debug": "7.10.0" } }, "node_modules/@prisma/get-platform": { @@ -30547,16 +30547,16 @@ } }, "node_modules/prisma": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", - "integrity": "sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", + "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@prisma/dev": "0.24.17", - "@prisma/engines": "7.9.1", + "@prisma/engines": "7.10.0", "@prisma/studio-core": "0.33.0", "mysql2": "3.15.3", "postgres": "3.4.7" diff --git a/package.json b/package.json index 62b73476c..f21c66d6b 100644 --- a/package.json +++ b/package.json @@ -91,8 +91,8 @@ "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", "@openrouter/ai-sdk-provider": "3.0.0", - "@prisma/adapter-pg": "7.9.1", - "@prisma/client": "7.9.1", + "@prisma/adapter-pg": "7.10.0", + "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.0", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", @@ -178,7 +178,7 @@ "@nx/storybook": "23.1.1", "@nx/web": "23.1.1", "@nx/workspace": "23.1.1", - "@prisma/config": "7.9.1", + "@prisma/config": "7.10.0", "@schematics/angular": "22.1.4", "@storybook/addon-docs": "10.5.7", "@storybook/addon-themes": "10.5.7", @@ -209,7 +209,7 @@ "nx": "23.1.1", "prettier": "3.9.6", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "7.9.1", + "prisma": "7.10.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "9.0.0", From 73e4f0368de398d7b67c50fb2926351414e2044e Mon Sep 17 00:00:00 2001 From: Pial-Al-Mamun Date: Wed, 2 Sep 2026 17:22:15 +0200 Subject: [PATCH 03/15] Task/improve language localization for IT (20260901) (#7782) * Update translations * Update changelog --- CHANGELOG.md | 1 + apps/client/src/locales/messages.it.xlf | 52 ++++++++++++------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3727101..855def05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) +- Improved the language localization for Italian (`it`) - Upgraded `prisma` from version `7.9.1` to `7.10.0` ### Fixed diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index c082eeee9..695acf32e 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -40,7 +40,7 @@ please - please + per favore apps/client/src/app/pages/pricing/pricing-page.html 333 @@ -92,7 +92,7 @@ with - with + con apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 87 @@ -100,7 +100,7 @@ Any person who has the link can view the data. - Any person who has the link can view the data. + Chiunque abbia il link può visualizzare i dati. apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 42 @@ -328,7 +328,7 @@ Paid - Paid + Pagamento effettuato apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts 136 @@ -368,7 +368,7 @@ Unknown - Unknown + Sconosciuto libs/ui/src/lib/i18n.ts 89 @@ -384,7 +384,7 @@ View and manage - View and manage + Visualizza e gestisci libs/ui/src/lib/access-level-icon/access-level-icon.component.html 9 @@ -420,7 +420,7 @@ and is driven by the efforts of its contributors - and is driven by the efforts of its contributors + ed è sostenuto dall’impegno dei suoi contributori apps/client/src/app/pages/about/overview/about-overview-page.html 50 @@ -428,7 +428,7 @@ Asset Profiles - Profilo dell’asset + Profili degli asset apps/client/src/app/components/admin-settings/admin-settings.component.html 131 @@ -492,7 +492,7 @@ Find an account... - Find an account... + Trova un account... libs/ui/src/lib/assistant/assistant.component.ts 448 @@ -528,7 +528,7 @@ Watch the Ghostfol.io Trailer on YouTube - Watch the Ghostfol.io Trailer on YouTube + Guarda il trailer di Ghostfol.io su YouTube apps/client/src/app/pages/landing/landing-page.html 19 @@ -644,7 +644,7 @@ Explore our guides to help you get started with investing and managing your finances. - Explore our guides to help you get started with investing and managing your finances. + Esplora le nostre guide per iniziare a investire e a gestire le tue finanze. apps/client/src/app/pages/resources/overview/resources-overview.component.ts 21 @@ -668,7 +668,7 @@ Update Account - Update Account + Aggiorna account libs/common/src/lib/routes/routes.ts 101 @@ -740,7 +740,7 @@ No auto-renewal on membership. - No auto-renewal on membership. + Nessun rinnovo automatico dell’abbonamento. apps/client/src/app/components/user-account-membership/user-account-membership.html 77 @@ -820,7 +820,7 @@ Creation - Creation + Creazione apps/client/src/app/components/admin-overview/admin-overview.html 191 @@ -876,7 +876,7 @@ Split Ratio - Split Ratio + Rapporto di frazionamento apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 532 @@ -884,7 +884,7 @@ Last Used - Last Used + Ultimo utilizzo apps/client/src/app/components/access-table/access-table.component.html 31 @@ -892,7 +892,7 @@ An error occurred while merging this asset profile into (). - An error occurred while merging this asset profile into (). + Si è verificato un errore durante l’unione di questo profilo di asset in (). apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 942 @@ -936,7 +936,7 @@ Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. - Find quick answers to commonly asked questions about Ghostfolio in our Frequently Asked Questions (FAQ) section. + Trova risposte rapide alle domande più frequenti su Ghostfolio nella nostra sezione FAQ. apps/client/src/app/pages/resources/overview/resources-overview.component.ts 16 @@ -992,7 +992,7 @@ Energy - Energy + Energia libs/ui/src/lib/i18n.ts 96 @@ -1220,7 +1220,7 @@ Ghostfolio in Numbers: Monthly Active Users (MAU) - Ghostfolio in Numbers: Monthly Active Users (MAU) + Ghostfolio in cifre: utenti attivi mensili (MAU) apps/client/src/app/pages/landing/landing-page.html 63 @@ -1240,7 +1240,7 @@ Performance with currency effect - Performance with currency effect + Performance con effetto valuta apps/client/src/app/pages/portfolio/analysis/analysis-page.html 125 @@ -1260,7 +1260,7 @@ Identifier has been copied to the clipboard - Identifier has been copied to the clipboard + Identificativo copiato negli appunti apps/client/src/app/components/access-table/access-table.component.ts 149 @@ -1340,7 +1340,7 @@ Add Account - Add Account + Aggiungi account libs/common/src/lib/routes/routes.ts 87 @@ -1356,7 +1356,7 @@ Consumer Defensive - Consumer Defensive + Beni di consumo difensivi libs/ui/src/lib/i18n.ts 95 @@ -1408,7 +1408,7 @@ Utilities - Utilities + Servizi di pubblica utilità libs/ui/src/lib/i18n.ts 103 @@ -9414,7 +9414,7 @@ Support Ghostfolio - Support Ghostfolio + Supporta Ghostfolio apps/client/src/app/pages/about/overview/about-overview-page.html 170 From 94c2c6cd06a94aaf2295159cb9dd5464688ea37d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:04:21 +0200 Subject: [PATCH 04/15] Task/migrate transfer cash balance dialog to dedicated route (#7791) * Migrate transfer cash balance dialog to dedicated route * Update changelog --- CHANGELOG.md | 1 + .../account-dialog-host.component.ts | 84 +++++++++++++++++- .../types/account-dialog-mode.type.ts | 3 +- .../pages/accounts/accounts-page.component.ts | 86 +------------------ .../src/app/pages/accounts/accounts-page.html | 1 - .../pages/accounts/accounts-page.routes.ts | 9 +- .../transfer-balance-dialog.component.ts | 4 +- libs/common/src/lib/routes/routes.ts | 5 ++ .../accounts-table.component.html | 2 +- .../accounts-table.component.ts | 8 +- 10 files changed, 106 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855def05a..04d33dca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) +- Migrated the transfer cash balance dialog to a dedicated route - Improved the language localization for Italian (`it`) - Upgraded `prisma` from version `7.9.1` to `7.10.0` diff --git a/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts index 29f7eb78b..0cc9476e3 100644 --- a/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts +++ b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts @@ -5,11 +5,17 @@ import { } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; +import { + CreateAccountDto, + TransferBalanceDto, + UpdateAccountDto +} from '@ghostfolio/common/dtos'; import { AccountResponse, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { Scope, hasScope, scopes } from '@ghostfolio/common/scopes'; +import { AccountWithPlatform } from '@ghostfolio/common/types'; +import { NotificationService } from '@ghostfolio/ui/notifications'; import { DataService } from '@ghostfolio/ui/services'; import { @@ -36,6 +42,8 @@ import { import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component'; import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces'; +import { TransferBalanceDialogParams } from '../transfer-balance/interfaces/interfaces'; +import { GfTransferBalanceDialogComponent } from '../transfer-balance/transfer-balance-dialog.component'; import { AccountDialogMode } from './types/account-dialog-mode.type'; @Component({ @@ -45,7 +53,9 @@ import { AccountDialogMode } from './types/account-dialog-mode.type'; }) export class GfAccountDialogHostComponent implements OnDestroy, OnInit { private dialogRef: MatDialogRef< - GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent + | GfAccountDetailDialogComponent + | GfCreateOrUpdateAccountDialogComponent + | GfTransferBalanceDialogComponent >; private readonly deviceType = computed(() => { @@ -61,6 +71,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { private readonly impersonationStorageService = inject( ImpersonationStorageService ); + private readonly notificationService = inject(NotificationService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly userService = inject(UserService); @@ -108,6 +119,24 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { return; } + if (mode === 'transferCashBalance') { + const accounts = user?.accounts ?? []; + + if ( + accounts.length < 2 || + !hasPermission(user?.permissions, permissions.updateAccount) || + this.isWriteRestricted(user, scopes.accountUpdate) + ) { + this.navigateBack(); + + return; + } + + this.openTransferBalanceDialog({ accounts }); + + return; + } + if (mode === 'update') { if ( !account || @@ -298,4 +327,55 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit { }); }); } + + private openTransferBalanceDialog({ + accounts + }: { + accounts: AccountWithPlatform[]; + }) { + const dialogRef = this.dialog.open< + GfTransferBalanceDialogComponent, + TransferBalanceDialogParams, + TransferBalanceDto | null + >(GfTransferBalanceDialogComponent, { + data: { + accounts + }, + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' + }); + + this.dialogRef = dialogRef; + + dialogRef + .afterClosed() + .pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { + if (!result) { + this.navigateBack(); + + return; + } + + this.dataService + .transferAccountBalance(result) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + error: () => { + this.notificationService.alert({ + title: $localize`Oops, cash balance transfer has failed.` + }); + + this.navigateBack(); + }, + next: () => { + // Deliberately not bound to the destroy reference: navigating + // back destroys this component and the refreshed user is what + // makes the accounts page reload its data + this.userService.get(true).subscribe(); + + this.navigateBack(); + } + }); + }); + } } diff --git a/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts b/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts index 9c2d44a2a..4cdf112a7 100644 --- a/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts +++ b/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts @@ -1 +1,2 @@ -export type AccountDialogMode = 'create' | 'detail' | 'update'; +export type AccountDialogMode = + 'create' | 'detail' | 'transferCashBalance' | 'update'; diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 2389cbaa8..f731962a5 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,7 +1,6 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; -import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { AccountsResponse, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; @@ -9,27 +8,19 @@ import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { AccountWithValue } from '@ghostfolio/common/types'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfFabComponent } from '@ghostfolio/ui/fab'; -import { NotificationService } from '@ghostfolio/ui/notifications'; import { DataService } from '@ghostfolio/ui/services'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - computed, DestroyRef, inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatDialog } from '@angular/material/dialog'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { DeviceDetectorService } from 'ngx-device-detector'; -import { filter, of, switchMap, tap } from 'rxjs'; -import { catchError } from 'rxjs/operators'; - -import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces'; -import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component'; +import { Router, RouterModule } from '@angular/router'; +import { filter, switchMap, tap } from 'rxjs'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -54,33 +45,15 @@ export class GfAccountsPageComponent implements OnInit { private hasImpersonationId: boolean; - private readonly deviceType = computed( - () => this.deviceDetectorService.deviceInfo().deviceType - ); - private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); - private readonly deviceDetectorService = inject(DeviceDetectorService); - private readonly dialog = inject(MatDialog); private readonly impersonationStorageService = inject( ImpersonationStorageService ); - private readonly notificationService = inject(NotificationService); - private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly userService = inject(UserService); - public constructor() { - this.route.queryParams - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((params) => { - if (params['transferBalanceDialog']) { - this.openTransferBalanceDialog(); - } - }); - } - public ngOnInit() { this.impersonationStorageService .onChangeHasImpersonation() @@ -135,67 +108,12 @@ export class GfAccountsPageComponent implements OnInit { }); } - protected onTransferBalance() { - this.router.navigate([], { - queryParams: { transferBalanceDialog: true } - }); - } - private fetchAccounts() { return this.dataService.fetchAccounts({ filters: this.userService.getFilters() }); } - private openTransferBalanceDialog() { - const dialogRef = this.dialog.open< - GfTransferBalanceDialogComponent, - TransferBalanceDialogParams - >(GfTransferBalanceDialogComponent, { - data: { - accounts: this.user?.accounts - }, - width: this.deviceType() === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((data: any) => { - if (data) { - this.reset(); - - const { accountIdFrom, accountIdTo, balance }: TransferBalanceDto = - data?.account; - - this.dataService - .transferAccountBalance({ - accountIdFrom, - accountIdTo, - balance - }) - .pipe( - catchError(() => { - this.notificationService.alert({ - title: $localize`Oops, cash balance transfer has failed.` - }); - - return of(undefined); - }), - switchMap(() => this.fetchAccounts()), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe((response) => { - this.updateAccounts(response); - }); - - this.changeDetectorRef.markForCheck(); - } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - private reset() { this.accounts = undefined; this.activitiesCount = 0; diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 55a8f6534..a9a344ce7 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -16,7 +16,6 @@ [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency" (accountDeleted)="onDeleteAccount($event)" - (transferBalance)="onTransferBalance()" /> diff --git a/apps/client/src/app/pages/accounts/accounts-page.routes.ts b/apps/client/src/app/pages/accounts/accounts-page.routes.ts index fd58d660c..74ebb3a77 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.routes.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.routes.ts @@ -6,7 +6,8 @@ import { Routes } from '@angular/router'; import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component'; import { GfAccountsPageComponent } from './accounts-page.component'; -const { create, detail, update } = internalRoutes.accounts.subRoutes; +const { create, detail, transferCashBalance, update } = + internalRoutes.accounts.subRoutes; export const routes: Routes = [ { @@ -18,6 +19,12 @@ export const routes: Routes = [ path: create.path, title: create.title }, + { + component: GfAccountDialogHostComponent, + data: { mode: 'transferCashBalance' }, + path: transferCashBalance.path, + title: transferCashBalance.title + }, { children: [ { diff --git a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts index 75f6f076a..cdfa7181c 100644 --- a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts @@ -99,13 +99,13 @@ export class GfTransferBalanceDialogComponent { } protected onSubmit() { - const account: TransferBalanceDto = { + const transferBalance: TransferBalanceDto = { accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '', accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '', balance: Number(this.transferBalanceForm.controls.balance.value) }; - this.dialogRef.close({ account }); + this.dialogRef.close(transferBalance); } private getAccountById(aId: string | null) { diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index 6985ba04f..3d7429760 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -93,6 +93,11 @@ export const internalRoutes = { }, title: $localize`Account` }, + transferCashBalance: { + path: 'transfer-cash-balance', + routerLink: ['/accounts', 'transfer-cash-balance'], + title: $localize`Transfer Cash Balance` + }, update: { path: 'update', routerLink: (aAccountId: string) => { diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 2719f05a0..a01abff1a 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -4,7 +4,7 @@ class="align-items-center d-flex" mat-stroked-button [disabled]="dataSource?.data.length < 2" - (click)="onTransferBalance()" + [routerLink]="transferCashBalanceRouterLink" > Transfer Cash Balance... diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index 48eb99d51..e98693290 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -75,7 +75,6 @@ export class GfAccountsTableComponent { public readonly totalValueInBaseCurrency = input(); public readonly accountDeleted = output(); - public readonly transferBalance = output(); public readonly sort = viewChild.required(MatSort); @@ -141,6 +140,9 @@ export class GfAccountsTableComponent { protected readonly isLoading = computed(() => !this.accounts()); + protected readonly transferCashBalanceRouterLink = + internalRoutes.accounts.subRoutes.transferCashBalance.routerLink; + private readonly notificationService = inject(NotificationService); private readonly router = inject(Router); @@ -197,8 +199,4 @@ export class GfAccountsTableComponent { title: aComment }); } - - protected onTransferBalance() { - this.transferBalance.emit(); - } } From 72b0d80411e2492321a796b8f7fbed114f0b4f35 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:17:33 +0200 Subject: [PATCH 05/15] Task/move details of granted access from table to dialog on access page (#7792) * Move details of granted access * Update changelog --- CHANGELOG.md | 1 + .../access-table/access-table.component.html | 38 ------------------- .../access-table/access-table.component.ts | 26 +------------ ...reate-or-update-access-dialog.component.ts | 37 +++++++++++++++++- .../create-or-update-access-dialog.html | 12 ++++++ 5 files changed, 49 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d33dca3..a0db33f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Moved the details of the granted access from the table to the dialog on the access page (experimental) - Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental) - Migrated the transfer cash balance dialog to a dedicated route - Improved the language localization for Italian (`it`) diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index fc324fb87..b8c2f8422 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -45,36 +45,6 @@ - - Details - - @if (element.type === 'PUBLIC') { - - @if (user()?.settings?.isExperimentalFeatures) { -
- GET {{ baseUrl }}/api/v1/public/{{ - element.id - }}/portfolio -
- } - } @else if (element.type === 'MCP' && hasPermissionToEnableMcp) { -
- {{ baseUrl }}{{ mcpEndpoint }} -
-
- Authorization: Bearer {{ element.id }} -
- } - -
- @@ -113,14 +83,6 @@ } - @if (element.type === 'MCP' && hasPermissionToEnableMcp) { - - } @if ( (!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) || element.type === 'PUBLIC' || diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index baf4247e3..9b666b3b6 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -1,4 +1,3 @@ -import { MCP_ENDPOINT } from '@ghostfolio/common/config'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { getDateFormatString } from '@ghostfolio/common/helper'; import { Access, User } from '@ghostfolio/common/interfaces'; @@ -32,7 +31,6 @@ import { copyOutline, createOutline, ellipsisHorizontal, - linkOutline, removeCircleOutline, trashOutline } from 'ionicons/icons'; @@ -81,14 +79,7 @@ export class GfAccessTableComponent { protected readonly dataSource = new MatTableDataSource(); protected readonly displayedColumns = computed(() => { - const columns = [ - 'alias', - 'grantee', - 'type', - 'lastUsedAt', - 'expiresAt', - 'details' - ]; + const columns = ['alias', 'grantee', 'type', 'lastUsedAt', 'expiresAt']; if (this.showActions()) { columns.push('actions'); @@ -109,8 +100,6 @@ export class GfAccessTableComponent { return !this.accesses(); }); - protected readonly mcpEndpoint = MCP_ENDPOINT; - private readonly clipboard = inject(Clipboard); private readonly dataService = inject(DataService); private readonly notificationService = inject(NotificationService); @@ -121,7 +110,6 @@ export class GfAccessTableComponent { copyOutline, createOutline, ellipsisHorizontal, - linkOutline, removeCircleOutline, trashOutline }); @@ -142,18 +130,6 @@ export class GfAccessTableComponent { return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`; } - protected onCopyIdToClipboard(aId: string) { - this.clipboard.copy(aId); - - this.snackBar.open( - '✅ ' + $localize`Identifier has been copied to the clipboard`, - undefined, - { - duration: ms('3 seconds') - } - ); - } - protected onCopyUrlToClipboard(aId: string) { this.clipboard.copy(this.getPublicUrl(aId)); diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 8c2bb0353..98275e5e7 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -1,5 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; +import { DEFAULT_LOCALE, MCP_ENDPOINT } from '@ghostfolio/common/config'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { canApplyFiltersToAccess } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; @@ -27,6 +27,7 @@ import { } from '@ghostfolio/ui/portfolio-filter-form'; import { DataService } from '@ghostfolio/ui/services'; +import { JsonPipe } from '@angular/common'; import type { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, @@ -69,6 +70,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; FormsModule, GfAccessLevelIconComponent, GfPortfolioFilterFormComponent, + JsonPipe, MatButtonModule, MatDatepickerModule, MatDialogModule, @@ -88,6 +90,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { public tags: Filter[] = []; protected accessForm: FormGroup; + protected readonly baseUrl = window.location.origin; protected minExpiresAt: Date; protected readonly mode: 'create' | 'update'; protected readonly today = startOfDay(new Date()); @@ -222,6 +225,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { this.loadHoldings(); } + protected get accessId() { + return this.data.access?.id; + } + protected get accessLevel(): AccessLevel { return this.accessForm?.get('accessLevel')?.value as AccessLevel; } @@ -234,10 +241,32 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { return this.accessType === 'PUBLIC'; } + protected get mcpConfiguration() { + return { + headers: { + Authorization: `Bearer ${this.accessId}` + }, + type: 'http', + url: `${this.baseUrl}${MCP_ENDPOINT}` + }; + } + protected get showExpiresAtErrorMessage() { return this.accessForm?.get('expiresAt')?.invalid === true; } + protected get showMcpDetails() { + return this.canGrantMcpAccess && this.isMcpAccess && this.mode === 'update'; + } + + protected get showPublicDetails() { + return ( + this.hasExperimentalFeatures && + this.isPublicAccess && + this.mode === 'update' + ); + } + protected onCancel() { this.dialogRef.close(); } @@ -250,6 +279,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } } + private get isMcpAccess() { + return this.accessType === 'MCP'; + } + private async createAccess() { const filters = this.getFilters(); @@ -343,7 +376,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit { } private async updateAccess() { - const accessId = this.data.access?.id; + const accessId = this.accessId; if (!accessId) { return; diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 21c0cd3a8..31391beb0 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -133,6 +133,18 @@ [tags]="tags" /> } + @if (showMcpDetails || showPublicDetails) { +

Details

+ @if (showMcpDetails) { +
{{ mcpConfiguration | json }}
+ } @else if (showPublicDetails) { +
+ GET {{ baseUrl }}/api/v1/public/{{ accessId }}/portfolio +
+ } + }