Browse Source

Merge branch 'main' into task/upgrade-papaparse-to-version-5.5.3

pull/6787/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
6daca79e1c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .gitignore
  2. 1
      .prettierignore
  3. 13
      CHANGELOG.md
  4. 1
      apps/api/src/app/endpoints/ai/ai.module.ts
  5. 13
      apps/api/src/app/endpoints/ai/ai.service.ts
  6. 31
      apps/api/src/app/health/health.controller.ts
  7. 2
      apps/api/src/app/health/health.module.ts
  8. 132
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  9. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  10. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  11. 2
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  12. 6
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  13. 14
      apps/client/src/app/components/admin-users/admin-users.html
  14. 72
      apps/client/src/app/components/header/header.component.html
  15. 2
      apps/client/src/app/components/header/header.component.ts
  16. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  17. 18
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  18. 2
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  19. 2
      apps/client/src/app/components/home-holdings/home-holdings.html
  20. 2
      apps/client/src/app/components/home-overview/home-overview.component.ts
  21. 7
      apps/client/src/app/components/home-overview/home-overview.html
  22. 4
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html
  23. 3
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  24. 11
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  25. 3
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  26. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  27. 10
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  28. 2
      apps/client/src/app/components/rule/rule.component.html
  29. 9
      apps/client/src/app/components/rule/rule.component.ts
  30. 3
      apps/client/src/app/pages/about/overview/about-overview-page.component.ts
  31. 2
      apps/client/src/app/pages/about/overview/about-overview-page.html
  32. 3
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  33. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  34. 65
      apps/client/src/app/pages/api/api-page.component.ts
  35. 244
      apps/client/src/app/pages/api/api-page.html
  36. 5
      apps/client/src/app/pages/api/interfaces/interfaces.ts
  37. 2
      apps/client/src/app/pages/landing/landing-page.component.ts
  38. 6
      apps/client/src/app/pages/landing/landing-page.html
  39. 2
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  40. 55
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  41. 4
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  42. 4
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  43. 2
      apps/client/src/app/pages/portfolio/fire/fire-page.html
  44. 7
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
  45. 2
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  46. 3
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  47. 4
      apps/client/src/app/pages/pricing/pricing-page.html
  48. 2
      apps/client/src/app/pages/public/public-page.component.ts
  49. 2
      apps/client/src/app/pages/public/public-page.html
  50. 12
      apps/client/src/styles.scss
  51. 2
      libs/common/src/lib/interfaces/index.ts
  52. 3
      libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts
  53. 6
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  54. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  55. 8
      libs/ui/src/lib/activities-table/activities-table.component.html
  56. 2
      libs/ui/src/lib/activity-type/activity-type.component.html
  57. 3
      libs/ui/src/lib/activity-type/activity-type.component.ts
  58. 6
      libs/ui/src/lib/benchmark/benchmark.component.html
  59. 2
      libs/ui/src/lib/benchmark/benchmark.component.ts
  60. 2
      libs/ui/src/lib/dialog-header/dialog-header.component.html
  61. 3
      libs/ui/src/lib/dialog-header/dialog-header.component.ts
  62. 2
      libs/ui/src/lib/entity-logo/entity-logo.component.html
  63. 2
      libs/ui/src/lib/entity-logo/entity-logo.component.ts
  64. 2
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html
  65. 2
      libs/ui/src/lib/logo/logo.component.html
  66. 2
      libs/ui/src/lib/logo/logo.component.ts
  67. 4
      libs/ui/src/lib/membership-card/membership-card.component.html
  68. 9
      libs/ui/src/lib/membership-card/membership-card.component.ts
  69. 4
      libs/ui/src/lib/toggle/toggle.component.html
  70. 3
      libs/ui/src/lib/toggle/toggle.component.ts
  71. 6
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  72. 2
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  73. 4
      libs/ui/src/lib/trend-indicator/trend-indicator.component.html
  74. 6
      libs/ui/src/lib/value/value.component.html
  75. 1598
      package-lock.json
  76. 38
      package.json

2
.gitignore

@ -32,6 +32,8 @@ npm-debug.log
.env.prod
.github/instructions/nx.instructions.md
.nx/cache
.nx/polygraph
.nx/self-healing
.nx/workspace-data
/.sass-cache
/connect.lock

1
.prettierignore

@ -1,5 +1,6 @@
/.agents/skills/angular-developer
/.nx/cache
/.nx/self-healing
/.nx/workspace-data
/apps/client/src/polyfills.ts
/dist

13
CHANGELOG.md

@ -9,8 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Migrated various components from `NgClass` to class bindings
- Refreshed the cryptocurrencies list
- Upgraded `@ionic/angular` from version `8.8.1` to `8.8.5`
- Upgraded `@openrouter/ai-sdk-provider` from version `0.7.2` to `2.9.0`
- Upgraded `ai` from version `4.3.16` to `6.0.174`
- Upgraded `bull-board` from version `6.20.3` to `7.0.0`
- Upgraded `countries-and-timezones` from version `3.8.0` to `3.9.0`
- Upgraded `fuse.js` from version `7.1.0` to `7.3.0`
- Upgraded `Nx` from version `22.6.5` to `22.7.1`
- Upgraded `papaparse` from version `5.3.1` to `5.5.3`
### Fixed
- Fixed a visual regression in the bottom navigation bar on mobile
## 3.2.0 - 2026-05-03
### Added

1
apps/api/src/app/endpoints/ai/ai.module.ts

@ -28,6 +28,7 @@ import { AiService } from './ai.service';
@Module({
controllers: [AiController],
exports: [AiService],
imports: [
ActivitiesModule,
ApiModule,

13
apps/api/src/app/endpoints/ai/ai.service.ts

@ -1,4 +1,5 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
PROPERTY_API_KEY_OPENROUTER,
@ -36,11 +37,18 @@ export class AiService {
];
public constructor(
private readonly configurationService: ConfigurationService,
private readonly portfolioService: PortfolioService,
private readonly propertyService: PropertyService
) {}
public async generateText({ prompt }: { prompt: string }) {
public async generateText({
prompt,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: {
prompt: string;
requestTimeout?: number;
}) {
const openRouterApiKey = await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_OPENROUTER
);
@ -55,7 +63,8 @@ export class AiService {
return generateText({
prompt,
model: openRouterService.chat(openRouterModel)
model: openRouterService.chat(openRouterModel),
timeout: requestTimeout
});
}

31
apps/api/src/app/health/health.controller.ts

@ -1,5 +1,7 @@
import { AiService } from '@ghostfolio/api/app/endpoints/ai/ai.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import {
AiServiceHealthResponse,
DataEnhancerHealthResponse,
DataProviderHealthResponse
} from '@ghostfolio/common/interfaces';
@ -9,6 +11,7 @@ import {
Get,
HttpException,
HttpStatus,
Logger,
Param,
Res,
UseInterceptors
@ -21,7 +24,10 @@ import { HealthService } from './health.service';
@Controller('health')
export class HealthController {
public constructor(private readonly healthService: HealthService) {}
public constructor(
private readonly aiService: AiService,
private readonly healthService: HealthService
) {}
@Get()
public async getHealth(@Res() response: Response) {
@ -40,6 +46,29 @@ export class HealthController {
}
}
@Get('ai')
public async getHealthOfAiService(
@Res() response: Response
): Promise<Response<AiServiceHealthResponse>> {
try {
const { text } = await this.aiService.generateText({
prompt: `Reply with the word "OK" and nothing else.`
});
if (text === 'OK') {
return response
.status(HttpStatus.OK)
.json({ status: getReasonPhrase(StatusCodes.OK) });
}
} catch (error) {
Logger.error(error, 'HealthController');
}
return response
.status(HttpStatus.SERVICE_UNAVAILABLE)
.json({ status: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE) });
}
@Get('data-enhancer/:name')
public async getHealthOfDataEnhancer(
@Param('name') name: string,

2
apps/api/src/app/health/health.module.ts

@ -1,3 +1,4 @@
import { AiModule } from '@ghostfolio/api/app/endpoints/ai/ai.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { DataEnhancerModule } from '@ghostfolio/api/services/data-provider/data-enhancer/data-enhancer.module';
@ -12,6 +13,7 @@ import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
imports: [
AiModule,
DataEnhancerModule,
DataProviderModule,
PropertyModule,

132
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -193,6 +193,7 @@
"AARK": "Aark",
"AART": "ALL.ART",
"AAST": "AASToken",
"AASTEROID": "Alien Asteroid",
"AAT": "Agricultural Trade Chain",
"AAVAWBTC": "Aave aWBTC",
"AAVE": "Aave",
@ -280,7 +281,7 @@
"ACTA": "Acta Finance",
"ACTIN": "Actinium",
"ACTN": "Action Coin",
"ACU": "ACU Platform",
"ACU": "Acurast Token",
"ACX": "Across Protocol",
"ACXT": "ACDX Exchange Token",
"ACYC": "All Coins Yield Capital",
@ -389,7 +390,7 @@
"AGATA": "Agatech",
"AGATOKEN": "AGA Token",
"AGB": "Apes Go Bananas",
"AGC": "Argocoin",
"AGC": "Alien Green Cat",
"AGEN": "Agent Krasnov",
"AGENT": "AgentLayer",
"AGENTFUN": "AgentFun.AI",
@ -417,6 +418,7 @@
"AGOV": "Answer Governance",
"AGPC": "AGPC",
"AGRI": "AgriDex Token",
"AGRICULTURALUNIONS": "Agricultural Unions",
"AGRO": "Bit Agro",
"AGRS": "Agoras Token",
"AGS": "Aegis",
@ -605,6 +607,7 @@
"ALBART": "Albärt",
"ALBE": "ALBETROS",
"ALBEDO": "ALBEDO",
"ALBON": "Albemarle (Ondo Tokenized)",
"ALBT": "AllianceBlock",
"ALC": "Arab League Coin",
"ALCAZAR": "Alcazar",
@ -700,6 +703,7 @@
"ALTMAN": "SAM",
"ALTOCAR": "AltoCar",
"ALTR": "Altranium",
"ALTSZN": "ALTSEASON",
"ALTT": "Altcoinist",
"ALU": "Altura",
"ALUSD": "Alchemix USD",
@ -994,6 +998,7 @@
"ARG": "Argentine Football Association Fan Token",
"ARGENTUM": "Argentum",
"ARGO": "ArGoApp",
"ARGOCOIN": "Argocoin",
"ARGON": "Argon",
"ARGUS": "ArgusCoin",
"ARI": "AriCoin",
@ -1123,10 +1128,11 @@
"ASTA": "ASTA",
"ASTER": "Aster",
"ASTERINU": "Aster INU",
"ASTEROID": "ASTEROID",
"ASTEROID": "Asteroid Shiba",
"ASTEROIDBOT": "Asteroid Bot",
"ASTEROIDCOIN": "ASTEROID",
"ASTEROIDETH": "Asteroid",
"ASTEROIDFIT": "ASTEROID",
"ASTHERUSUSDF": "Astherus USDF",
"ASTO": "Altered State Token",
"ASTON": "Aston",
@ -1396,7 +1402,6 @@
"BABI": "Babylons",
"BABL": "Babylon Finance",
"BABY": "Babylon",
"BABY4": "Baby 4",
"BABYANDY": "Baby Andy",
"BABYASTER": "Baby Aster",
"BABYB": "Baby Bali",
@ -1778,7 +1783,7 @@
"BDID": "BDID",
"BDIN": "BendDAO BDIN",
"BDL": "Bitdeal",
"BDOG": "Bulldog Token",
"BDOG": "BurnDog",
"BDOGITO": "BullDogito",
"BDOT": "Binance Wrapped DOT",
"BDP": "Big Data Protocol",
@ -1846,6 +1851,7 @@
"BELA": "Bela",
"BELG": "Belgian Malinois",
"BELIEVE": "Believe",
"BELKA": "The Dancing Squirrel",
"BELL": "Bellscoin",
"BELLE": "Isabelle",
"BELLS": "Bellscoin",
@ -1882,8 +1888,9 @@
"BERN": "BERNcash",
"BERNIE": "BERNIE SENDERS",
"BERRIE": "Berrie Token",
"BERRY": "Berry",
"BERRY": "Strawberry AI",
"BERRYS": "BerrySwap",
"BERRYSTORE": "Berry",
"BERT": "Bertram The Pomeranian",
"BES": "battle esports coin",
"BESA": "Besa Gaming",
@ -1935,6 +1942,7 @@
"BGB": "Bitget token",
"BGBG": "BigMouthFrog",
"BGBP": "Binance GBP Stable Coin",
"BGBTC": "Bitget Wrapped BTC",
"BGBV1": "Bitget Token v1",
"BGC": "Bee Token",
"BGCI": "Bloomberg Galaxy Crypto Index",
@ -1989,6 +1997,7 @@
"BIDI": "Bidipass",
"BIDP": "BID Protocol",
"BIDR": "Binance IDR Stable Coin",
"BIDUON": "Baidu (Ondo Tokenized)",
"BIDZ": "BIDZ Coin",
"BIDZV1": "BIDZ Coin v1",
"BIFI": "Beefy.Finance",
@ -2133,6 +2142,7 @@
"BITTO": "BITTO",
"BITTON": "Bitton",
"BITTY": "The Bitcoin Mascot",
"BITUBU": "UBU",
"BITUPTOKEN": "BitUP Token",
"BITUSD": "bitUSD",
"BITV": "Bitvolt",
@ -2196,6 +2206,7 @@
"BLC": "BlakeCoin",
"BLCT": "Bloomzed Loyalty Club Ticket",
"BLD": "Agoric",
"BLEND": "Fluent",
"BLENDR": "Blendr Network",
"BLEPE": "Blepe",
"BLERF": "BLERF",
@ -2218,7 +2229,7 @@
"BLKC": "BlackHat Coin",
"BLKD": "Blinked",
"BLKS": "Blockshipping",
"BLM": "Blombard",
"BLM": "BLM coin",
"BLN": "Bulleon",
"BLNM": "Bolenum",
"BLOB": "B.O.B the Blob",
@ -2247,6 +2258,7 @@
"BLOCX": "BLOCX.",
"BLOGGE": "Bloggercube",
"BLOK": "Bloktopia",
"BLOMBARD": "Blombard",
"BLOO": "bloo foster coin",
"BLOOCYS": "BlooCYS",
"BLOODY": "Bloody Token",
@ -2880,6 +2892,7 @@
"BTFA": "Banana Task Force Ape",
"BTG": "Bitcoin Gold",
"BTGON": "B2Gold (Ondo Tokenized)",
"BTGOON": "BitGo Holdings (Ondo Tokenized)",
"BTH": "Bithereum",
"BTK": "Bostoken",
"BTL": "Bitlocus",
@ -2973,11 +2986,12 @@
"BUL": "bul",
"BULDAK": "Buldak",
"BULEI": "Bulei",
"BULL": "Tron Bull",
"BULL": "Bull",
"BULLA": "BULLA",
"BULLBEAR": "BullBear AI",
"BULLC": "BuySell",
"BULLDOG": "BullDog Coin",
"BULLDOGTOKEN": "Bulldog Token",
"BULLF": "BULL FINANCE",
"BULLGOD": "Bull God",
"BULLI": "Bullish On Ethereum",
@ -3392,7 +3406,6 @@
"CDOGE": "cyberdoge",
"CDPT": "Creditor Data Platform",
"CDRX": "CDRX",
"CDT": "CheckDot",
"CDX": "CDX Network",
"CDY": "Bitcoin Candy",
"CDragon": "Clumsy Dragon",
@ -3513,6 +3526,7 @@
"CHC": "ChainCoin",
"CHD": "CharityDAO",
"CHECK": "Checkmate",
"CHECKDOT": "CheckDot",
"CHECKR": "CheckerChain",
"CHECOIN": "CheCoin",
"CHED": "Giggleched",
@ -3573,6 +3587,7 @@
"CHINU": "Chubby Inu",
"CHIP": "Chip",
"CHIPI": "chipi",
"CHIPP": "Chip",
"CHIPPY": "Chippy",
"CHIPS": "CHIPS",
"CHIRP": "Chirp Token",
@ -4350,6 +4365,7 @@
"CVX": "Convex Finance",
"CVXCRV": "Convex CRV",
"CVXFXS": "Convex FXS",
"CVXON": "Chevron (Ondo Tokenized)",
"CVXX": "Chevron xStock",
"CW": "CardWallet",
"CWA": "Chris World Asset",
@ -4741,6 +4757,7 @@
"DEPAY": "DePay",
"DEPIN": "DEPIN",
"DEPINU": "Depression Inu",
"DEPLOYR": "Deployr",
"DEPO": "Depo",
"DEPTH": "Depth Token",
"DEQ": "Dequant",
@ -5363,6 +5380,7 @@
"DUA": "Brillion",
"DUAL": "DUAL",
"DUALDAOTOKEN": "Dual Finance",
"DUALV1": "BLOCKv",
"DUB": "DubCoin",
"DUBAICAT": "Dubai Cat",
"DUBBZ": "Dubbz",
@ -5548,6 +5566,7 @@
"ECHO": "Echo",
"ECHOBOT": "ECHO BOT",
"ECHOD": "EchoDEX",
"ECHON": "iShares MSCI Chile ETF (Ondo Tokenized)",
"ECHT": "e-Chat",
"ECI": "Euro Cup Inu",
"ECKODAO": "eckoDAO",
@ -6110,6 +6129,7 @@
"EVERGROW": "EverGrowCoin",
"EVERLIFE": "EverLife.AI",
"EVERMOON": "EverMoon",
"EVERRISE": "EverRise",
"EVERV": "EverValue Coin",
"EVERY": "Everyworld",
"EVIL": "EvilCoin",
@ -6160,6 +6180,7 @@
"EXN": "Exeno",
"EXNT": "EXNT",
"EXO": "Exosis",
"EXODON": "Exodus Movement (Ondo Tokenized)",
"EXOS": "Exobots",
"EXP": "Expanse",
"EXPAND": "Gems",
@ -6213,6 +6234,7 @@
"FACTR": "Defactor",
"FACTRPAY": "FactR",
"FACY": "ArAIstotle Fact Checker",
"FADEWALLET": "FadeWallet Token",
"FADO": "FADO Go",
"FAFO": "FAFO",
"FAFOSOL": "Fafo",
@ -6312,6 +6334,7 @@
"FCT": "FirmaChain",
"FCTC": "FaucetCoin",
"FCTR": "FactorDAO",
"FCXON": "Freeport-McMoRan (Ondo Tokenized)",
"FDC": "FDrive Coin",
"FDGC": "FINTECH DIGITAL GOLD COIN",
"FDLS": "FIDELIS",
@ -6364,6 +6387,7 @@
"FFCT": "FortFC",
"FFM": "Files.fm Library",
"FFN": "Fairy Forest",
"FFOGON": "Franklin Focused Growth ETF (Ondo Tokenized)",
"FFTP": "FIGHT FOR THE PEOPLE",
"FFUEL": "getFIFO",
"FFYI": "Fiscus FYI",
@ -6535,6 +6559,7 @@
"FLOOR": "FloorDAO",
"FLOP": "Big Floppa",
"FLOPPA": "Floppa Cat",
"FLOR": "FLORK",
"FLORK": "FLORK BNB",
"FLORKY": "Florky",
"FLOSHIDO": "FLOSHIDO INU",
@ -6549,6 +6574,7 @@
"FLOWP": "Flow Protocol",
"FLOYX": "Floyx",
"FLP": "Gameflip",
"FLQLON": "Franklin US Large Cap Multifactor Index ETF (Ondo Tokenized)",
"FLR": "Flare",
"FLRBRG": "Floor Cheese Burger",
"FLRS": "Flourish Coin",
@ -6802,6 +6828,7 @@
"FSN": "Fusion",
"FSNV1": "Fusion v1",
"FSO": "FSociety",
"FSOLON": "Fidelity Solana Fund (Ondo Tokenized)",
"FST": "FreeStyle Token",
"FSTC": "FastCoin",
"FSTR": "Fourth Star",
@ -7131,6 +7158,7 @@
"GENS": "Genshiro",
"GENSLR": "Good Gensler",
"GENSTAKE": "Genstake",
"GENSYN": "Gensyn",
"GENT": "Gentleman",
"GENX": "Genx Token",
"GENXNET": "Genesis Network",
@ -7313,6 +7341,7 @@
"GLR": "Glory Finance",
"GLS": "Glacier",
"GLT": "GlobalToken",
"GLTRON": "abrdn Physical Precious Metals Basket Shares ETF (Ondo Tokenized)",
"GLUE": "Glue",
"GLX": "GalaxyCoin",
"GLYPH": "GlyphCoin",
@ -7569,10 +7598,11 @@
"GRIDZ": "GridZone.io",
"GRIFFAIN": "GRIFFAIN",
"GRIFT": "ORBIT",
"GRIM": "GRIMREAPER",
"GRIM": "GrimHustle",
"GRIMACE": "Grimace",
"GRIMEVO": "Grim EVO",
"GRIMEX": "SpaceGrime",
"GRIMREAPER": "GRIMREAPER",
"GRIN": "Grin",
"GRIND": "Self Improving",
"GRIPPY": "GRIPPY",
@ -7955,6 +7985,7 @@
"HEWE": "Health & Wealth",
"HEX": "HEX",
"HEXC": "HexCoin",
"HEYFLORK": "HeyFlork",
"HEZ": "Hermez Network Token",
"HF": "Have Fun",
"HFI": "Holder Finance",
@ -8348,11 +8379,12 @@
"IBG": "iBG Token",
"IBGT": "Infrared BGT",
"IBIT": "InfinityBit Token",
"IBITON": "iShares Bitcoin Trust (Ondo Tokenized)",
"IBMX": "International Business Machines xStock",
"IBNB": "iBNB",
"IBP": "Innovation Blockchain Payment",
"IBS": "Irbis Network",
"IC": "Ignition",
"IC": "Icy",
"ICA": "Icarus Network",
"ICAP": "ICAP Token",
"ICASH": "ICASH",
@ -8442,6 +8474,7 @@
"IGGT": "The Invincible Game Token",
"IGI": "Igi",
"IGNIS": "Ignis",
"IGNITION": "Ignition",
"IGT": "Infinitar",
"IGTT": "IGT",
"IGU": "IguVerse",
@ -8496,7 +8529,6 @@
"IMS": "Independent Money System",
"IMST": "Imsmart",
"IMT": "Immortal Token",
"IMU": "Immunefi",
"IMUSIFY": "imusify",
"IMVR": "ImmVRse",
"IMX": "Immutable X",
@ -8509,11 +8541,13 @@
"INCEPT": "Incept",
"INCNT": "Incent",
"INCO": "InfinitiCoin",
"INCOME": "Universal High Income",
"INCORGNITO": "Incorgnito",
"INCP": "InceptionCoin",
"INCREMENTUM": "Incrementum",
"INCX": "INCX Coin",
"IND": "Indorse",
"INDAON": "iShares MSCI India ETF (Ondo Tokenized)",
"INDAY": "Independence Day",
"INDEPENDENCEDAY": "Independence Day",
"INDEX": "Index Cooperative",
@ -8621,6 +8655,7 @@
"IONC": "IONChain",
"IONOMY": "Ionomy",
"IONP": "Ion Power Token",
"IONQON": "IonQ (Ondo Tokenized)",
"IONX": "Charged Particles",
"IONZ": "IONZ",
"IOP": "Internet of People",
@ -8704,6 +8739,7 @@
"ITALOCOIN": "Italocoin",
"ITAM": "ITAM Games",
"ITAMCUBE": "CUBE",
"ITAON": "iShares US Aerospace and Defense ETF (Ondo Tokenized)",
"ITC": "IoT Chain",
"ITE": "Idle Tribe Era",
"ITEM": "ITEMVERSE",
@ -9377,6 +9413,7 @@
"KONET": "KONET",
"KONG": "KONG",
"KONO": "Konomi Network",
"KOON": "Coca-Cola (Ondo Tokenized)",
"KORA": "Kortana",
"KORC": "King of Referral Coin",
"KORE": "KORE Vault",
@ -9490,6 +9527,7 @@
"KWAI": "KWAI",
"KWATT": "4New",
"KWD": "KIWI DEFI",
"KWEBON": "KraneShares CSI China Internet ETF (Ondo Tokenized)",
"KWEEN": "KWEEN",
"KWENTA": "Kwenta",
"KWH": "KWHCoin",
@ -9877,6 +9915,7 @@
"LMR": "Lumerin",
"LMT": "LIMITUS",
"LMTOKEN": "LM Token",
"LMTON": "Lockheed (Ondo Tokenized)",
"LMTS": "Limitless Official Token",
"LMWR": "LimeWire Token",
"LMXC": "LimonX",
@ -10118,6 +10157,7 @@
"LUNES": "Lunes",
"LUNG": "LunaGens",
"LUNR": "Lunr Token",
"LUNRON": "Intuitive Machines (Ondo Tokenized)",
"LUPIN": "LUPIN",
"LUR": "Lumera",
"LUS": "Luna Rush",
@ -10295,7 +10335,8 @@
"MANUSAI": "Manus AI Agent",
"MANYU": "Manyu",
"MANYUDOG": "MANYU",
"MAO": "Mao",
"MAO": "MAO",
"MAOMEME": "Mao",
"MAOW": "MAOW",
"MAP": "MAP Protocol",
"MAPC": "MapCoin",
@ -11573,6 +11614,7 @@
"NBAR": "NOBAR",
"NBC": "Niobium",
"NBD": "Never Back Down",
"NBISON": "Nebius Group (Ondo Tokenized)",
"NBIT": "NetBit",
"NBL": "Nobility",
"NBLU": "NuriTopia",
@ -11649,6 +11691,7 @@
"NEKOS": "Nekocoin",
"NEKTAR": "Nektar Token",
"NEMO": "NEMO",
"NEMON": "Newmont (Ondo Tokenized)",
"NEMS": "The Nemesis",
"NEO": "NEO",
"NEOG": "NEO Gold",
@ -11938,6 +11981,7 @@
"NOVA": "Nova Finance",
"NOVAAI": "Nova AI",
"NOW": "NOW Token",
"NOWON": "ServiceNow (Ondo Tokenized)",
"NOX": "NITRO",
"NOXB": "Noxbox",
"NPAS": "New Paradigm Assets Solution",
@ -12218,6 +12262,7 @@
"OHNOGG": "OHNHO (ohno.gg)",
"OHO": "OHO",
"OICOIN": "Osmium Investment Coin",
"OIHON": "VanEck Oil Services ETF (Ondo Tokenized)",
"OIIAOIIA": "spinning cat",
"OIK": "Space Nation",
"OIL": "Oiler",
@ -12314,8 +12359,9 @@
"ONION": "DeepOnion",
"ONIT": "ONBUFF",
"ONIX": "Onix",
"ONL": "On.Live",
"ONL": "OneLink",
"ONLINE": "Onlinebase",
"ONLIVE": "On.Live",
"ONLY": "OnlyCam",
"ONLYCUMIES": "OnlyCumies",
"ONLYFANSCOINS": "$OFC Coin",
@ -12375,6 +12421,7 @@
"OPES": "Opes",
"OPET": "ÕpetFoundation",
"OPEX": "Optherium Token",
"OPG": "OpenGradient",
"OPHX": "Operation Phoenix",
"OPINU": "Optimus Inu",
"OPIUM": "Opium",
@ -12675,6 +12722,7 @@
"PATRIOT": "Patriot",
"PATTON": "Patton",
"PAUL": "Elephant Penguin",
"PAVEON": "Global X US Infrastructure Development ETF (Ondo Tokenized)",
"PAVIA": "Pavia",
"PAVO": "Pavocoin",
"PAW": "PAWSWAP",
@ -13397,6 +13445,7 @@
"PPI": "Primpy",
"PPIZZA": "P Pizza",
"PPL": "Pink Panther Lovers",
"PPLTON": "abrdn Physical Platinum Shares ETF (Ondo Tokenized)",
"PPM": "Punk Panda Messenger",
"PPN": "Puppies Network",
"PPOVR": "POVR",
@ -13443,6 +13492,7 @@
"PRIMATE": "Primate",
"PRIME": "Echelon Prime",
"PRIMECHAIN": "PrimeChain",
"PRIMECOIN": "PrimeCoin",
"PRIMEETH": "Prime Staked ETH",
"PRIMEX": "Primex Finance",
"PRIN": "Print The Pepe",
@ -13792,6 +13842,7 @@
"QUBE": "Qube",
"QUBIC": "Qubic",
"QUBITICA": "Qubitica",
"QUBTON": "Quantum Computing (Ondo Tokenized)",
"QUBY": "Quby",
"QUDEFI": "Qudefi",
"QUE": "Queen Of Memes",
@ -13966,6 +14017,7 @@
"RDR": "Rise of Defenders",
"RDS": "Reger Diamond",
"RDT": "Ridotto",
"RDWON": "Redwire (Ondo Tokenized)",
"RDX": "Redux Protocol",
"REA": "Realisto",
"REACH": "/Reach",
@ -14028,6 +14080,7 @@
"REFI": "Realfinance Network",
"REFLECT": "REFLECT",
"REFLECTO": "Reflecto",
"REFLECTOUSD": "Reflecto USD",
"REFTOKEN": "RefToken",
"REFUND": "Refund",
"REG": "RealToken Ecosystem Governance",
@ -14037,6 +14090,7 @@
"REGEN": "Regen Network",
"REGENT": "REGENT COIN",
"REGI": "Resistance Girl",
"REGNON": "Regeneron Pharmaceuticals (Ondo Tokenized)",
"REGRET": "Regret",
"REHA": "Resistance Hamster",
"REHAB": "NFT Rehab",
@ -14202,7 +14256,7 @@
"RIPT": "RiptideCoin",
"RIPTO": "RiptoBuX",
"RIS": "Riser",
"RISE": "EverRise",
"RISE": "Rise NASA",
"RISECOIN": "Rise coin",
"RISEP": "Rise Protocol",
"RISEVISION": "Rise",
@ -14227,6 +14281,7 @@
"RKC": "Royal Kingdom Coin",
"RKEY": "RKEY",
"RKI": "RAKHI",
"RKLBON": "Rocket Lab (Ondo Tokenized)",
"RKN": "RAKON",
"RKR": "REAKTOR",
"RKT": "Rock Token",
@ -14426,6 +14481,7 @@
"RUGPROOF": "Launchpad",
"RUGPULL": "Captain Rug Pull",
"RUGZ": "pulltherug.finance",
"RUJI": "Rujira",
"RULER": "Ruler Protocol",
"RUM": "RUM Pirates of The Arrland Token",
"RUN": "Speedrun",
@ -14440,7 +14496,7 @@
"RUP": "Rupee",
"RUPX": "Rupaya",
"RURI": "Ruri - Truth Terminal's Crush",
"RUSD": "Reflecto USD",
"RUSD": "Royal Dollar",
"RUSH": "RUSH COIN",
"RUSHCMC": "RUSHCMC",
"RUSSELL": "Russell",
@ -14659,6 +14715,7 @@
"SBSC": "Subscriptio",
"SBT": "SOLBIT",
"SBTC": "Super Bitcoin",
"SBUXON": "Starbucks (Ondo Tokenized)",
"SC": "Siacoin",
"SC20": "Shine Chain",
"SCA": "Scallop",
@ -14675,6 +14732,7 @@
"SCASH": "SpaceCash",
"SCAT": "Sad Cat Token",
"SCC": "StockChain Coin",
"SCCOON": "Southern Copper (Ondo Tokenized)",
"SCCP": "S.C. Corinthians Fan Token",
"SCDS": "Shrine Cloud Storage Network",
"SCF": "Smoking Chicken Fish",
@ -15054,6 +15112,7 @@
"SHX": "Stronghold Token",
"SHXV1": "Stronghold Token v1",
"SHY": "Shytoshi Kusama",
"SHYON": "iShares 1-3 Year Treasury Bond ETF (Ondo Tokenized)",
"SHYTCOIN": "ShytCoin",
"SI": "Siren",
"SI14": "Si14",
@ -15248,6 +15307,7 @@
"SLUGDENG": "SLUG DENG",
"SLUMBO": "SLUMBO",
"SLVLUSD": "Staked Level USD",
"SLVN": "SLVNToken",
"SLVON": "iShares Silver Trust (Ondo Tokenized)",
"SLVX": "eToro Silver",
"SLX": "SLIMEX",
@ -15339,6 +15399,7 @@
"SNC": "SunContract",
"SNCT": "SnakeCity",
"SND": "Sandcoin",
"SNDKON": "SanDisk (Ondo Tokenized)",
"SNE": "StrongNode",
"SNEED": "Sneed",
"SNEK": "Snek",
@ -15632,10 +15693,11 @@
"SPIDERMAN": "SPIDERMAN BITCOIN",
"SPIDEY": "Spidey",
"SPIK": "Spike",
"SPIKE": "Spiking",
"SPIKE": "SPIKE",
"SPIKE1984": "Spike 1984",
"SPIKECOIN": "SPIKE",
"SPIKEFURIE": "SPIKE",
"SPIKING": "Spiking",
"SPILLWAYS": "SpillWays",
"SPIN": "SPIN Protocol",
"SPINT": "Spintria",
@ -15934,6 +15996,7 @@
"STRA": "STRAY",
"STRAKS": "Straks",
"STRAT": "Strategic Hub for Innovation in Blockchain",
"STRAWBE": "Strawberry In Bloom",
"STRAX": "Stratis",
"STRAY": "Stray Dog",
"STRAYDOG": "Stray Dog",
@ -15981,6 +16044,7 @@
"STV": "Sativa Coin",
"STWEMIX": "Staked WEMIX",
"STX": "Stacks",
"STXON": "Seagate (Ondo Tokenized)",
"STYL": "Stylike Governance",
"STYLE": "Style",
"STZ": "99Starz",
@ -16801,6 +16865,7 @@
"TRADECHAIN": "Trade Chain",
"TRADETIDE": "Trade Tide Token",
"TRADEX": "TradeX AI",
"TRADIX": "Tradix",
"TRADOOR": "Tradoor",
"TRAI": "Trackgood AI",
"TRAID": "Traid",
@ -16881,6 +16946,7 @@
"TROLLRUN": "TROLL",
"TROLLS": "trolls in a memes world",
"TRONBETLIVE": "TRONbetLive",
"TRONBULL": "Tron Bull",
"TRONDOG": "TronDog",
"TRONI": "Tron Inu",
"TRONP": "Donald Tronp",
@ -17015,6 +17081,7 @@
"TTM": "Tradetomato",
"TTN": "TTN",
"TTNT": "TITA Project",
"TTPA": "TRUMPTOPIA",
"TTT": "TRUMPETTOKEN",
"TTTU": "T-Project",
"TTU": "TaTaTu",
@ -17077,7 +17144,7 @@
"TWP": "TrumpWifPanda",
"TWT": "Trust Wallet Token",
"TWURTLE": "twurtle the turtle",
"TX": "Tradix",
"TX": "tx",
"TX20": "Trex20",
"TXA": "TXA",
"TXAG": "tSILVER",
@ -17160,6 +17227,7 @@
"UDT": "Unlock Protocol",
"UE": "UE Coin",
"UEC": "United Emirates Coin",
"UECON": "Uranium Energy (Ondo Tokenized)",
"UEDC": "United Emirate Decentralized Coin",
"UENC": "UniversalEnergyChain",
"UET": "Useless Ethereum Token",
@ -17239,6 +17307,7 @@
"UNF": "Unfed Coin",
"UNFI": "Unifi Protocol DAO",
"UNFK": "UNFK",
"UNGON": "US Natural Gas Fund (Ondo Tokenized)",
"UNHX": "UnitedHealth xStock",
"UNI": "Uniswap Protocol Token",
"UNIART": "UNIART",
@ -17276,6 +17345,7 @@
"UNITE": "Unite",
"UNITED": "UnitedCoins",
"UNITEDTRADERS": "United Traders Token",
"UNITOKEN": "Uni Token",
"UNITPROV2": "Unit Protocol New",
"UNITRADE": "UniTrade",
"UNITREEAI": "Unitree G1 AI",
@ -17292,17 +17362,18 @@
"UNO": "UnoRe",
"UNOB": "Unobtanium",
"UNP": "UNIPOLY",
"UNPON": "Union Pacific Corporation (Ondo Tokenized)",
"UNQ": "UNQ",
"UNQT": "Unique Utility Token",
"UNR": "Unirealchain",
"UNRC": "UniversalRoyalCoin",
"UNS": "UNS TOKEN",
"UNSHETH": "unshETH Ether",
"UNT": "Uni Token",
"UNT": "UnityWallet Token",
"UNW": "UniWorld",
"UOP": "Utopia Genesis Foundation",
"UOS": "UOS",
"UP": "UpToken",
"UP": "Superform",
"UPC": "UPCX",
"UPCG": "Upcomings",
"UPCO2": "Universal Carbon",
@ -17317,6 +17388,7 @@
"UPRO": "ULTRAPRO",
"UPS": "UPFI Network",
"UPT": "UPROCK",
"UPTOKEN": "UpToken",
"UPTOP": "UPTOP",
"UPTOS": "UPTOS",
"UPUNK": "Unicly CryptoPunks Collection",
@ -17327,6 +17399,7 @@
"URAC": "Uranus",
"URALS": "Urals Coin",
"URANUS": "Uranus",
"URAON": "Global X Uranium ETF (Ondo Tokenized)",
"URFA": "Urfaspor Token",
"URMOM": "urmom",
"URO": "Urolithin A",
@ -17575,7 +17648,7 @@
"VEC2": "VectorCoin 2.0",
"VECT": "Vectorium",
"VECTOR": "VectorChat.ai",
"VEE": "BLOCKv",
"VEE": "Vee Token",
"VEED": "VEED",
"VEEN": "LIVEEN",
"VEETOKEN": "Vee Token",
@ -17638,6 +17711,7 @@
"VEXT": "Veloce",
"VFIL": "Venus Filecoin",
"VFOX": "VFOX",
"VFSON": "VinFast Auto (Ondo Tokenized)",
"VFT": "Value Finance",
"VFX": "ViFoxCoin",
"VFY": "zkVerify",
@ -17758,6 +17832,7 @@
"VNM": "Venom",
"VNN": "VINU Network",
"VNO": "Veno Finance",
"VNQON": "Vanguard Real Estate ETF (Ondo Tokenized)",
"VNST": "VNST Stablecoin",
"VNT": "VNT Chain",
"VNTR": "Venture Mind AI",
@ -17831,6 +17906,7 @@
"VRSW": "VirtuSwap",
"VRT": "Venus Reward Token",
"VRTX": "Vertex Protocol",
"VRTXON": "Vertex Pharmaceuticals (Ondo Tokenized)",
"VRTY": "Verity",
"VRX": "Verox",
"VS": "veSync",
@ -17844,6 +17920,7 @@
"VSOL": "VSolidus",
"VSP": "Vesper Finance",
"VSTA": "Vesta Finance",
"VSTON": "Vistra (Ondo Tokenized)",
"VSTR": "Vestra DAO",
"VSUI": "Volo Staked SUI",
"VSX": "Versus-X",
@ -18038,6 +18115,7 @@
"WCUSD": "Wrapped Celo Dollar",
"WDAI": "Dai (Wormhole)",
"WDC": "WorldCoin",
"WDCON": "Western Digital (Ondo Tokenized)",
"WDOG": "Winterdog",
"WDOGE": "Wrapped Dogecoin",
"WDOT": "WDOT",
@ -18273,6 +18351,7 @@
"WMM": "Weird Medieval Memes",
"WMN": "WebMind Network",
"WMNT": "Wrapped Mantle",
"WMON": "Waste Management (Ondo Tokenized)",
"WMOXY": "Moxy",
"WMT": "World Mobile Token v1",
"WMTON": "Walmart (Ondo Tokenized)",
@ -18304,9 +18383,10 @@
"WOID": "WORLD ID",
"WOJ": "Wojak Finance",
"WOJA": "Wojak",
"WOJAK": "Wojak",
"WOJAK": "wojak",
"WOJAK2": "Wojak 2.0 Coin",
"WOJAKC": "Wojak Coin",
"WOJAKIO": "Wojak",
"WOKB": "Wrapped OKB",
"WOKIE": "Wokie Plumpkin by Virtuals",
"WOKT": "Wrapped OKT",
@ -18462,6 +18542,7 @@
"WXRP": "Wrapped XRP",
"WXT": "WXT",
"WYAC": "Woman Yelling At Cat",
"WYDE": "WYDE: End Hunger",
"WYN": "Wynn",
"WYNN": "Anita Max Wynn",
"WYS": "Wysker",
@ -18683,6 +18764,7 @@
"XMN": "xMoney",
"XMO": "Monero Original",
"XMON": "XMON",
"XMONEY": "X Money",
"XMOON": "r/CryptoCurrency Moons v1",
"XMP": "Mapt.Coin",
"XMR": "Monero",
@ -18741,7 +18823,7 @@
"XPL": "Plasma",
"XPLA": "XPLA",
"XPLL": "ParallelChain",
"XPM": "PrimeCoin",
"XPM": "XPMarket Token",
"XPN": "PANTHEON X",
"XPND": "Time Raiders",
"XPNET": "XP Network",
@ -18860,6 +18942,7 @@
"XVS": "Venus",
"XWC": "WhiteCoin",
"XWG": "X World Games",
"XWGT": "Wodo Gaming Token",
"XWIN": "xWIN Finance",
"XWP": "Swap",
"XWT": "World Trade Funds",
@ -19102,7 +19185,8 @@
"ZEBU": "ZEBU",
"ZEC": "ZCash",
"ZECD": "ZCashDarkCoin",
"ZED": "ZedCoins",
"ZED": "ZED Token",
"ZEDCOIN": "ZedCoin",
"ZEDD": "ZedDex",
"ZEDTOKEN": "Zed Token",
"ZEDX": "ZEDX Сoin",

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -20,7 +20,6 @@ import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -57,7 +56,6 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfAccountBalancesComponent,
GfActivitiesTableComponent,
GfDialogFooterComponent,

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -91,8 +91,8 @@
<mat-tab-group
animationDuration="0ms"
[class.d-none]="isLoadingActivities"
[mat-stretch-tabs]="false"
[ngClass]="{ 'd-none': isLoadingActivities }"
>
<mat-tab>
<ng-template mat-tab-label>

2
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -172,7 +172,7 @@
<ion-icon
class="h6 mb-0"
name="time-outline"
[ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }"
[class.text-danger]="element.stacktrace?.length > 0"
/>
} @else if (element.state === 'failed') {
<ion-icon

6
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -309,11 +309,11 @@
</div>
<mat-paginator
[class.d-none]="
(isLoading && totalItems === 0) || totalItems <= pageSize
"
[hidePageSize]="true"
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
}"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"

14
apps/client/src/app/components/admin-users/admin-users.html

@ -19,16 +19,12 @@
<div class="d-flex align-items-center">
<span
class="d-none d-sm-inline-block text-monospace"
[ngClass]="{
'text-line-through': element.role === 'INACTIVE'
}"
[class.text-line-through]="element.role === 'INACTIVE'"
>{{ element.id }}</span
>
<span
class="d-inline-block d-sm-none text-monospace"
[ngClass]="{
'text-line-through': element.role === 'INACTIVE'
}"
[class.text-line-through]="element.role === 'INACTIVE'"
>{{ `${(element.id | slice: 0 : 5)}...` }}</span
>
@if (element.subscription?.expiresAt) {
@ -273,11 +269,11 @@
</div>
<mat-paginator
[class.d-none]="
(isLoading && totalItems === 0) || totalItems <= pageSize
"
[hidePageSize]="true"
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
}"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"

72
apps/client/src/app/components/header/header.component.html

@ -1,10 +1,10 @@
<mat-toolbar class="px-0">
@if (user) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs">
<a
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0"
mat-button
[ngClass]="{ 'w-100': hasTabs }"
[class.w-100]="hasTabs"
[routerLink]="['/']"
(click)="onLogoClick()"
>
@ -18,7 +18,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold':
currentRoute === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path,
@ -35,7 +35,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === internalRoutes.portfolio.path,
'text-decoration-underline':
currentRoute === internalRoutes.portfolio.path
@ -49,7 +49,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === internalRoutes.accounts.path,
'text-decoration-underline':
currentRoute === internalRoutes.accounts.path
@ -64,7 +64,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold':
currentRoute === internalRoutes.adminControl.path,
'text-decoration-underline':
@ -80,7 +80,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routeResources,
'text-decoration-underline': currentRoute === routeResources
}"
@ -95,7 +95,7 @@
<a
class="d-none d-sm-block rounded"
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing
}"
@ -115,7 +115,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout
}"
@ -239,11 +239,10 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold':
currentRoute === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path
}"
[class.font-weight-bold]="
currentRoute === internalRoutes.home.path ||
currentRoute === internalRoutes.zen.path
"
[routerLink]="['/']"
>Overview</a
>
@ -251,9 +250,9 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === internalRoutes.portfolio.path
}"
[class.font-weight-bold]="
currentRoute === internalRoutes.portfolio.path
"
[routerLink]="routerLinkPortfolio"
>Portfolio</a
>
@ -261,18 +260,18 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === internalRoutes.accounts.path
}"
[class.font-weight-bold]="
currentRoute === internalRoutes.accounts.path
"
[routerLink]="routerLinkAccounts"
>Accounts</a
>
<a
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === internalRoutes.account.path
}"
[class.font-weight-bold]="
currentRoute === internalRoutes.account.path
"
[routerLink]="routerLinkAccount"
>My Ghostfolio</a
>
@ -281,10 +280,9 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold':
currentRoute === internalRoutes.adminControl.path
}"
[class.font-weight-bold]="
currentRoute === internalRoutes.adminControl.path
"
[routerLink]="routerLinkAdminControl"
>Admin Control</a
>
@ -294,9 +292,7 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{
'font-weight-bold': currentRoute === routeResources
}"
[class.font-weight-bold]="currentRoute === routeResources"
[routerLink]="routerLinkResources"
>Resources</a
>
@ -306,7 +302,7 @@
<a
class="d-flex d-sm-none"
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
[class.font-weight-bold]="currentRoute === routePricing"
[routerLink]="routerLinkPricing"
>
<span class="align-items-center d-flex">
@ -321,7 +317,7 @@
class="d-flex d-sm-none"
i18n
mat-menu-item
[ngClass]="{ 'font-weight-bold': currentRoute === routeAbout }"
[class.font-weight-bold]="currentRoute === routeAbout"
[routerLink]="routerLinkAbout"
>About Ghostfolio</a
>
@ -332,11 +328,11 @@
</ul>
}
@if (user === null) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs">
<a
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0"
mat-button
[ngClass]="{ 'w-100': hasTabs }"
[class.w-100]="hasTabs"
[routerLink]="['/']"
>
<gf-logo
@ -352,7 +348,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routeFeatures,
'text-decoration-underline': currentRoute === routeFeatures
}"
@ -365,7 +361,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout
}"
@ -378,7 +374,7 @@
<a
class="d-sm-block rounded"
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing
}"
@ -399,7 +395,7 @@
class="d-none d-sm-block rounded"
i18n
mat-button
[ngClass]="{
[class]="{
'font-weight-bold': currentRoute === routeMarkets,
'text-decoration-underline': currentRoute === routeMarkets
}"

2
apps/client/src/app/components/header/header.component.ts

@ -19,7 +19,6 @@ import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -56,7 +55,6 @@ import { catchError } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfAssistantComponent,
GfLogoComponent,
GfPremiumIndicatorComponent,

2
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -29,7 +29,6 @@ import { DataService } from '@ghostfolio/ui/services';
import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -76,7 +75,6 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfAccountsTableComponent,
GfActivitiesTableComponent,
GfDataProviderCreditsComponent,

18
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -117,13 +117,12 @@
<gf-value
i18n
size="medium"
[class.text-danger]="
marketPriceMin?.toFixed(2) === marketPrice?.toFixed(2) &&
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
"
[isCurrency]="true"
[locale]="data.locale"
[ngClass]="{
'text-danger':
marketPriceMin?.toFixed(2) === marketPrice?.toFixed(2) &&
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
}"
[precision]="marketPriceMinPrecision"
[unit]="SymbolProfile?.currency"
[value]="marketPriceMin"
@ -134,13 +133,12 @@
<gf-value
i18n
size="medium"
[class.text-success]="
marketPriceMax?.toFixed(2) === marketPrice?.toFixed(2) &&
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
"
[isCurrency]="true"
[locale]="data.locale"
[ngClass]="{
'text-success':
marketPriceMax?.toFixed(2) === marketPrice?.toFixed(2) &&
marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2)
}"
[precision]="marketPriceMaxPrecision"
[unit]="SymbolProfile?.currency"
[value]="marketPriceMax"

2
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -14,7 +14,6 @@ import { DataService } from '@ghostfolio/ui/services';
import { GfToggleComponent } from '@ghostfolio/ui/toggle';
import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -34,7 +33,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
imports: [
CommonModule,
FormsModule,
GfHoldingsTableComponent,
GfToggleComponent,

2
apps/client/src/app/components/home-holdings/home-holdings.html

@ -44,7 +44,7 @@
(treemapChartClicked)="onHoldingClicked($event)"
/>
}
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
<div [class.d-none]="viewModeFormControl.value !== 'TABLE'">
<gf-holdings-table
[holdings]="holdings"
[locale]="user?.settings?.locale"

2
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -14,7 +14,6 @@ import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -29,7 +28,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
imports: [
CommonModule,
GfLineChartComponent,
GfPortfolioPerformanceComponent,
MatButtonModule,

7
apps/client/src/app/components/home-overview/home-overview.html

@ -11,10 +11,7 @@
<h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p>
<ol class="font-weight-bold">
<li
class="mb-2"
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
>
<li class="mb-2" [class.text-muted]="user?.accounts?.length > 1">
<a class="d-block" [routerLink]="routerLinkAccounts"
><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n
@ -70,13 +67,13 @@
<gf-line-chart
class="position-absolute"
unit="%"
[class.pr-3]="deviceType === 'mobile'"
[colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
[label]="performanceLabel"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"

4
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html

@ -24,11 +24,11 @@
}
<div
class="display-4 font-weight-bold m-0 text-center value-container"
[hidden]="isLoading"
[ngClass]="{
[class]="{
'text-danger': isAllTimeLow,
'text-success': isAllTimeHigh
}"
[hidden]="isLoading"
>
<span #value id="value"></span>
</div>

3
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -10,7 +10,6 @@ import {
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -28,7 +27,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, GfValueComponent, IonIcon, NgxSkeletonLoaderModule],
imports: [GfValueComponent, IonIcon, NgxSkeletonLoaderModule],
selector: 'gf-portfolio-performance',
styleUrls: ['./portfolio-performance.component.scss'],
templateUrl: './portfolio-performance.component.html'

11
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -181,12 +181,11 @@
</div>
<div
class="align-items-center d-flex justify-content-end"
[ngClass]="{
'cursor-pointer':
hasPermissionToUpdateUserSettings &&
!user?.settings?.isRestrictedView &&
user?.subscription?.type !== 'Basic'
}"
[class.cursor-pointer]="
hasPermissionToUpdateUserSettings &&
!user?.settings?.isRestrictedView &&
user?.subscription?.type !== 'Basic'
"
(click)="
hasPermissionToUpdateUserSettings &&
!user?.settings?.isRestrictedView &&

3
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -5,7 +5,6 @@ import { translate } from '@ghostfolio/ui/i18n';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -25,7 +24,7 @@ import {
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, GfValueComponent, IonIcon, MatTooltipModule],
imports: [GfValueComponent, IonIcon, MatTooltipModule],
selector: 'gf-portfolio-summary',
styleUrls: ['./portfolio-summary.component.scss'],
templateUrl: './portfolio-summary.component.html'

2
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -1,7 +1,6 @@
import { XRayRulesSettings } from '@ghostfolio/common/interfaces';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@ -16,7 +15,6 @@ import { RuleSettingsDialogParams } from './interfaces/interfaces';
@Component({
imports: [
CommonModule,
FormsModule,
GfValueComponent,
MatButtonModule,

10
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html

@ -47,10 +47,7 @@
</div>
</div>
} @else {
<div
class="w-100"
[ngClass]="{ 'd-none': !data.rule.configuration.thresholdMin }"
>
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin">
<h6 class="d-flex mb-0">
<ng-container i18n>Threshold Min</ng-container>:
<gf-value
@ -85,10 +82,7 @@
/>
</div>
</div>
<div
class="w-100"
[ngClass]="{ 'd-none': !data.rule.configuration.thresholdMax }"
>
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax">
<h6 class="d-flex mb-0">
<ng-container i18n>Threshold Max</ng-container>:
<gf-value

2
apps/client/src/app/components/rule/rule.component.html

@ -14,7 +14,7 @@
} @else {
<div
class="align-items-center d-flex icon-container mr-2 px-2"
[ngClass]="{
[class]="{
okay: rule?.value === true,
warn: rule?.value === false
}"

9
apps/client/src/app/components/rule/rule.component.ts

@ -5,7 +5,6 @@ import {
XRayRulesSettings
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -37,13 +36,7 @@ import { GfRuleSettingsDialogComponent } from './rule-settings-dialog/rule-setti
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
IonIcon,
MatButtonModule,
MatMenuModule,
NgxSkeletonLoaderModule
],
imports: [IonIcon, MatButtonModule, MatMenuModule, NgxSkeletonLoaderModule],
selector: 'gf-rule',
styleUrls: ['./rule.component.scss'],
templateUrl: './rule.component.html'

3
apps/client/src/app/pages/about/overview/about-overview-page.component.ts

@ -4,7 +4,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -26,7 +25,7 @@ import {
} from 'ionicons/icons';
@Component({
imports: [CommonModule, IonIcon, MatButtonModule, RouterModule],
imports: [IonIcon, MatButtonModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-about-overview-page',
styleUrls: ['./about-overview-page.scss'],

2
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -178,7 +178,7 @@
<div class="mb-5 row">
<div
class="col-md-6 col-xs-12 my-2"
[ngClass]="{ 'offset-md-3': hasPermissionForSubscription === false }"
[class.offset-md-3]="hasPermissionForSubscription === false"
>
<a
class="py-4 w-100"

3
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -4,7 +4,7 @@ import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule, NgClass } from '@angular/common';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
AbstractControl,
@ -43,7 +43,6 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
MatDialogModule,
MatFormFieldModule,
MatInputModule,
NgClass,
ReactiveFormsModule
],
selector: 'gf-create-or-update-account-dialog',

2
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -43,7 +43,7 @@
}}</span>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': platforms?.length < 1 }">
<div [class.d-none]="platforms?.length < 1">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Platform</mat-label>
<input

65
apps/client/src/app/pages/api/api-page.component.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AiServiceHealthResponse,
DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse,
DividendsResponse,
@ -13,27 +14,42 @@ import {
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import {
HttpClient,
HttpErrorResponse,
HttpHeaders,
HttpParams
} from '@angular/common/http';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card';
import { format, startOfYear } from 'date-fns';
import { map, Observable } from 'rxjs';
import { isObject } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { catchError, map, Observable, of, OperatorFunction } from 'rxjs';
import { FetchFailure, FetchResult } from './interfaces/interfaces';
@Component({
host: { class: 'page' },
imports: [CommonModule],
imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule],
selector: 'gf-api-page',
styleUrls: ['./api-page.scss'],
templateUrl: './api-page.html'
})
export class GfApiPageComponent implements OnInit {
public assetProfile$: Observable<DataProviderGhostfolioAssetProfileResponse>;
public dividends$: Observable<DividendsResponse['dividends']>;
public historicalData$: Observable<HistoricalResponse['historicalData']>;
public isinLookupItems$: Observable<LookupResponse['items']>;
public lookupItems$: Observable<LookupResponse['items']>;
public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>;
public aiServiceHealth$: Observable<FetchResult<AiServiceHealthResponse>>;
public assetProfile$: Observable<
FetchResult<DataProviderGhostfolioAssetProfileResponse>
>;
public dividends$: Observable<FetchResult<DividendsResponse['dividends']>>;
public historicalData$: Observable<
FetchResult<HistoricalResponse['historicalData']>
>;
public isinLookupItems$: Observable<FetchResult<LookupResponse['items']>>;
public lookupItems$: Observable<FetchResult<LookupResponse['items']>>;
public quotes$: Observable<FetchResult<QuotesResponse['quotes']>>;
public status$: Observable<FetchResult<DataProviderGhostfolioStatusResponse>>;
private apiKey: string;
@ -45,6 +61,7 @@ export class GfApiPageComponent implements OnInit {
public ngOnInit() {
this.apiKey = prompt($localize`Please enter your Ghostfolio API key:`);
this.aiServiceHealth$ = this.fetchAiServiceHealth();
this.assetProfile$ = this.fetchAssetProfile({ symbol: 'AAPL' });
this.dividends$ = this.fetchDividends({ symbol: 'KO' });
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' });
@ -54,13 +71,33 @@ export class GfApiPageComponent implements OnInit {
this.status$ = this.fetchStatus();
}
public isFetchFailure(value: unknown): value is FetchFailure {
return isObject(value) && value !== null && 'fetchError' in value;
}
private catchFetchFailure<T>(): OperatorFunction<T, T | FetchFailure> {
return catchError(({ error }: HttpErrorResponse) => {
const body = error as { message?: string; status?: string };
const status = body?.status ?? 'Error';
const fetchError = body?.message ? `${status}: ${body.message}` : status;
return of<FetchFailure>({ fetchError });
}) as OperatorFunction<T, T | FetchFailure>;
}
private fetchAiServiceHealth() {
return this.http
.get<AiServiceHealthResponse>('/api/v1/health/ai')
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchAssetProfile({ symbol }: { symbol: string }) {
return this.http
.get<DataProviderGhostfolioAssetProfileResponse>(
`/api/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{ headers: this.getHeaders() }
)
.pipe(takeUntilDestroyed(this.destroyRef));
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private fetchDividends({ symbol }: { symbol: string }) {
@ -80,6 +117,7 @@ export class GfApiPageComponent implements OnInit {
map(({ dividends }) => {
return dividends;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -101,6 +139,7 @@ export class GfApiPageComponent implements OnInit {
map(({ historicalData }) => {
return historicalData;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -127,6 +166,7 @@ export class GfApiPageComponent implements OnInit {
map(({ items }) => {
return items;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -143,6 +183,7 @@ export class GfApiPageComponent implements OnInit {
map(({ quotes }) => {
return quotes;
}),
this.catchFetchFailure(),
takeUntilDestroyed(this.destroyRef)
);
}
@ -153,7 +194,7 @@ export class GfApiPageComponent implements OnInit {
'/api/v2/data-providers/ghostfolio/status',
{ headers: this.getHeaders() }
)
.pipe(takeUntilDestroyed(this.destroyRef));
.pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef));
}
private getHeaders() {

244
apps/client/src/app/pages/api/api-page.html

@ -1,77 +1,173 @@
<div class="container">
<div class="mb-3">
<h2 class="text-center">Status</h2>
<div>{{ status$ | async | json }}</div>
</div>
<div class="mb-3">
<h2 class="text-center">Asset Profile</h2>
<div>{{ assetProfile$ | async | json }}</div>
</div>
<div>
<h2 class="text-center">Lookup</h2>
@if (lookupItems$) {
@let symbols = lookupItems$ | async;
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Lookup (ISIN)</h2>
@if (isinLookupItems$) {
@let symbols = isinLookupItems$ | async;
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Quotes</h2>
@if (quotes$) {
@let quotes = quotes$ | async;
<ul>
@for (quote of quotes | keyvalue; track quote) {
<li>
{{ quote.key }}: {{ quote.value.marketPrice }}
{{ quote.value.currency }}
</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Historical</h2>
@if (historicalData$) {
@let historicalData = historicalData$ | async;
<ul>
@for (
historicalDataItem of historicalData | keyvalue;
track historicalDataItem
) {
<li>
{{ historicalDataItem.key }}:
{{ historicalDataItem.value.marketPrice }}
</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Dividends</h2>
@if (dividends$) {
@let dividends = dividends$ | async;
<ul>
@for (dividend of dividends | keyvalue; track dividend) {
<li>
{{ dividend.key }}:
{{ dividend.value.marketPrice }}
</li>
}
</ul>
}
<div class="mb-5 row">
<div class="col">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>AI Service Health</mat-card-title>
</mat-card-header>
<mat-card-content>
@let aiServiceHealth = aiServiceHealth$ | async;
@if (isFetchFailure(aiServiceHealth)) {
🔴 {{ aiServiceHealth.fetchError }}
} @else if (aiServiceHealth) {
🟢 {{ aiServiceHealth.status }}
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Status</mat-card-title>
</mat-card-header>
<mat-card-content>
@let status = status$ | async;
@if (isFetchFailure(status)) {
🔴 {{ status.fetchError }}
} @else if (status) {
<pre><code>{{ status | json }}</code></pre>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Asset Profile</mat-card-title>
</mat-card-header>
<mat-card-content>
@let assetProfile = assetProfile$ | async;
@if (isFetchFailure(assetProfile)) {
🔴 {{ assetProfile.fetchError }}
} @else if (assetProfile) {
<pre><code>{{ assetProfile | json }}</code></pre>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Lookup</mat-card-title>
</mat-card-header>
<mat-card-content>
@let symbols = lookupItems$ | async;
@if (isFetchFailure(symbols)) {
🔴 {{ symbols.fetchError }}
} @else if (symbols) {
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Lookup (ISIN)</mat-card-title>
</mat-card-header>
<mat-card-content>
@let isinSymbols = isinLookupItems$ | async;
@if (isFetchFailure(isinSymbols)) {
🔴 {{ isinSymbols.fetchError }}
} @else if (isinSymbols) {
<ul>
@for (item of isinSymbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Quotes</mat-card-title>
</mat-card-header>
<mat-card-content>
@let quotes = quotes$ | async;
@if (isFetchFailure(quotes)) {
🔴 {{ quotes.fetchError }}
} @else if (quotes) {
<ul>
@for (quote of quotes | keyvalue; track quote) {
<li>
{{ quote.key }}: {{ quote.value.marketPrice }}
{{ quote.value.currency }}
</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Historical</mat-card-title>
</mat-card-header>
<mat-card-content>
@let historicalData = historicalData$ | async;
@if (isFetchFailure(historicalData)) {
🔴 {{ historicalData.fetchError }}
} @else if (historicalData) {
<ul>
@for (item of historicalData | keyvalue; track item) {
<li>{{ item.key }}: {{ item.value.marketPrice }}</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title>Dividends</mat-card-title>
</mat-card-header>
<mat-card-content>
@let dividends = dividends$ | async;
@if (isFetchFailure(dividends)) {
🔴 {{ dividends.fetchError }}
} @else if (dividends) {
<ul>
@for (dividend of dividends | keyvalue; track dividend) {
<li>{{ dividend.key }}: {{ dividend.value.marketPrice }}</li>
}
</ul>
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{ height: '1.5rem', width: '100%' }"
/>
}
</mat-card-content>
</mat-card>
</div>
</div>
</div>

5
apps/client/src/app/pages/api/interfaces/interfaces.ts

@ -0,0 +1,5 @@
export interface FetchFailure {
fetchError: string;
}
export type FetchResult<T> = T | FetchFailure;

2
apps/client/src/app/pages/landing/landing-page.component.ts

@ -8,7 +8,6 @@ import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@ -25,7 +24,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfCarouselComponent,
GfLogoCarouselComponent,
GfLogoComponent,

6
apps/client/src/app/pages/landing/landing-page.html

@ -54,7 +54,7 @@
<div class="row mb-5">
<div
class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
[class.justify-content-center]="deviceType !== 'mobile'"
>
<a
class="d-block"
@ -72,7 +72,7 @@
</div>
<div
class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
[class.justify-content-center]="deviceType !== 'mobile'"
>
<a
class="d-block"
@ -90,7 +90,7 @@
</div>
<div
class="col-md-4 d-flex my-1"
[ngClass]="{ 'justify-content-center': deviceType !== 'mobile' }"
[class.justify-content-center]="deviceType !== 'mobile'"
>
<a
class="d-block"

2
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -16,7 +16,6 @@ import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplet
import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@ -70,7 +69,6 @@ import { ActivityType } from './types/activity-type.type';
MatFormFieldModule,
MatInputModule,
MatSelectModule,
NgClass,
ReactiveFormsModule
],
selector: 'gf-create-or-update-activity-dialog',

55
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -76,11 +76,12 @@
</mat-select>
</mat-form-field>
</div>
<div [ngClass]="{ 'mb-3': mode === 'update' }">
<div [class.mb-3]="mode === 'update'">
<mat-form-field
appearance="outline"
class="w-100"
[ngClass]="{ 'mb-1 without-hint': mode === 'create' }"
[class.mb-1]="mode === 'create'"
[class.without-hint]="mode === 'create'"
>
<mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId">
@ -103,18 +104,16 @@
</mat-select>
</mat-form-field>
</div>
<div class="mb-3" [ngClass]="{ 'd-none': mode === 'update' }">
<div class="mb-3" [class.d-none]="mode === 'update'">
<mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
>Update Cash Balance</mat-checkbox
>
</div>
<div
class="mb-3"
[ngClass]="{
'd-none': !activityForm
.get('searchSymbol')
?.hasValidator(Validators.required)
}"
[class.d-none]="
!activityForm.get('searchSymbol')?.hasValidator(Validators.required)
"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label>
@ -127,9 +126,9 @@
</div>
<div
class="mb-3"
[ngClass]="{
'd-none': !activityForm.get('name')?.hasValidator(Validators.required)
}"
[class.d-none]="
!activityForm.get('name')?.hasValidator(Validators.required)
"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label>
@ -173,13 +172,12 @@
</div>
<div
class="mb-3"
[ngClass]="{
'd-none':
activityForm.get('type')?.value === 'FEE' ||
activityForm.get('type')?.value === 'INTEREST' ||
activityForm.get('type')?.value === 'ITEM' ||
activityForm.get('type')?.value === 'LIABILITY'
}"
[class.d-none]="
activityForm.get('type')?.value === 'FEE' ||
activityForm.get('type')?.value === 'INTEREST' ||
activityForm.get('type')?.value === 'ITEM' ||
activityForm.get('type')?.value === 'LIABILITY'
"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label>
@ -188,7 +186,7 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.get('type')?.value === 'FEE' }"
[class.d-none]="activityForm.get('type')?.value === 'FEE'"
>
<div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100">
@ -215,7 +213,7 @@
<div
class="ml-2"
matTextSuffix
[ngClass]="{ 'd-none': !activityForm.get('currency')?.value }"
[class.d-none]="!activityForm.get('currency')?.value"
>
<mat-select formControlName="currencyOfUnitPrice">
@for (currency of currencies; track currency) {
@ -247,12 +245,11 @@
</div>
<div
class="mb-3"
[ngClass]="{
'd-none':
activityForm.get('type')?.value === 'INTEREST' ||
activityForm.get('type')?.value === 'ITEM' ||
activityForm.get('type')?.value === 'LIABILITY'
}"
[class.d-none]="
activityForm.get('type')?.value === 'INTEREST' ||
activityForm.get('type')?.value === 'ITEM' ||
activityForm.get('type')?.value === 'LIABILITY'
"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
@ -260,7 +257,7 @@
<div
class="ml-2"
matTextSuffix
[ngClass]="{ 'd-none': !activityForm.get('currency')?.value }"
[class.d-none]="!activityForm.get('currency')?.value"
>
{{ activityForm.get('currencyOfUnitPrice')?.value }}
</div>
@ -280,7 +277,7 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.get('type')?.value !== 'ITEM' }"
[class.d-none]="activityForm.get('type')?.value !== 'ITEM'"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Asset Class</mat-label>
@ -299,7 +296,7 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.get('type')?.value !== 'ITEM' }"
[class.d-none]="activityForm.get('type')?.value !== 'ITEM'"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Asset Sub Class</mat-label>

4
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -21,7 +21,6 @@ import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
import { NgClass } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -51,8 +50,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
GfValueComponent,
GfWorldMapChartComponent,
MatCardModule,
MatProgressBarModule,
NgClass
MatProgressBarModule
],
selector: 'gf-allocations-page',
styleUrls: ['./allocations-page.scss'],

4
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -319,9 +319,7 @@
</div>
<div
class="col-md-12"
[ngClass]="{
'd-none': !user?.settings?.isExperimentalFeatures
}"
[class.d-none]="!user?.settings?.isExperimentalFeatures"
>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">

2
apps/client/src/app/pages/portfolio/fire/fire-page.html

@ -62,7 +62,7 @@
/>
</div>
} @else {
<div [ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }">
<div [class.text-muted]="user?.subscription?.type === 'Basic'">
<div class="mb-2">
<ng-container i18n
>If you retire today, you would be able to withdraw</ng-container

7
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html

@ -30,7 +30,7 @@
} @else {
<div
class="align-items-center d-flex icon-container mr-2 px-2"
[ngClass]="{
[class]="{
okay:
statistics?.rulesFulfilledCount ===
statistics?.rulesActiveCount,
@ -60,10 +60,7 @@
}
</div>
@for (category of categories; track category.key) {
<div
class="mb-4"
[ngClass]="{ 'd-none': category.rules?.length === 0 }"
>
<div class="mb-4" [class.d-none]="category.rules?.length === 0">
<h4 class="align-items-center d-flex m-0">
<span>{{ category.name }}</span>
@if (user?.subscription?.type === 'Basic') {

2
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -12,7 +12,6 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services';
import { NgClass } from '@angular/common';
import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { IonIcon } from '@ionic/angular/standalone';
@ -29,7 +28,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
GfPremiumIndicatorComponent,
GfRulesComponent,
IonIcon,
NgClass,
NgxSkeletonLoaderModule
],
selector: 'gf-x-ray-page',

3
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -7,7 +7,6 @@ import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { DataService } from '@ghostfolio/ui/services';
import { CommonModule } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@ -35,7 +34,6 @@ import { catchError } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfPremiumIndicatorComponent,
IonIcon,
MatButtonModule,
@ -78,6 +76,7 @@ export class GfPricingPageComponent implements OnInit {
'Interactive Brokers',
'Mintos',
'Monefit SmartSaver',
'Revolut',
'Swissquote',
'VIAC',
'Zak'

4
apps/client/src/app/pages/pricing/pricing-page.html

@ -103,7 +103,7 @@
<mat-card
appearance="outlined"
class="h-100"
[ngClass]="{ active: user?.subscription?.type === 'Basic' }"
[class.active]="user?.subscription?.type === 'Basic'"
>
<mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1">
@ -169,7 +169,7 @@
<mat-card
appearance="outlined"
class="h-100"
[ngClass]="{ active: user?.subscription?.type === 'Premium' }"
[class.active]="user?.subscription?.type === 'Premium'"
>
<mat-card-content class="d-flex flex-column h-100">
@if (label) {

2
apps/client/src/app/pages/public/public-page.component.ts

@ -14,7 +14,6 @@ import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
import { CommonModule } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectorRef,
@ -40,7 +39,6 @@ import { catchError } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfActivitiesTableComponent,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,

2
apps/client/src/app/pages/public/public-page.html

@ -202,7 +202,7 @@
</div>
</div>
}
<div class="row" [ngClass]="{ 'd-none': hasPermissionForSubscription }">
<div class="row" [class.d-none]="hasPermissionForSubscription">
<div class="col-md-12">
<mat-card appearance="outlined">
<mat-card-header class="overflow-hidden w-100">

12
apps/client/src/styles.scss

@ -520,15 +520,15 @@ ngx-skeleton-loader {
padding: 2rem 0;
}
@include mat.tabs-overrides(
(
divider-height: 0
)
);
@media (min-width: 576px) {
flex-direction: row-reverse;
@include mat.tabs-overrides(
(
divider-height: 0
)
);
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base), 0.02);
padding: 2rem 0;

2
libs/common/src/lib/interfaces/index.ts

@ -44,6 +44,7 @@ import type { ActivityResponse } from './responses/activity-response.interface';
import type { AdminUserResponse } from './responses/admin-user-response.interface';
import type { AdminUsersResponse } from './responses/admin-users-response.interface';
import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
@ -117,6 +118,7 @@ export {
AdminUserResponse,
AdminUsersResponse,
AiPromptResponse,
AiServiceHealthResponse,
ApiKeyResponse,
AssertionCredentialJSON,
AssetClassSelectorOption,

3
libs/common/src/lib/interfaces/responses/ai-service-health-response.interface.ts

@ -0,0 +1,3 @@
export interface AiServiceHealthResponse {
status: string;
}

6
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -340,15 +340,13 @@
<tr
*matRowDef="let row; columns: displayedColumns()"
mat-row
[ngClass]="{
'cursor-pointer': hasPermissionToOpenDetails()
}"
[class.cursor-pointer]="hasPermissionToOpenDetails()"
(click)="onOpenAccountDetailDialog(row.id)"
></tr>
<tr
*matFooterRowDef="displayedColumns()"
mat-footer-row
[ngClass]="{ 'd-none': isLoading() || !showFooter() }"
[class.d-none]="isLoading() || !showFooter()"
></tr>
</table>
</div>

2
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -4,7 +4,6 @@ import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -37,7 +36,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfEntityLogoComponent,
GfValueComponent,
IonIcon,

8
libs/ui/src/lib/activities-table/activities-table.component.html

@ -516,9 +516,7 @@
<tr
*matRowDef="let row; columns: displayedColumns()"
mat-row
[ngClass]="{
'cursor-pointer': canClickActivity(row)
}"
[class.cursor-pointer]="canClickActivity(row)"
(click)="onClickActivity(row)"
></tr>
</table>
@ -536,11 +534,9 @@
}
<mat-paginator
[class.d-none]="(isLoading() && !totalItems) || totalItems <= pageSize"
[hidePageSize]="true"
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading() && !totalItems) || totalItems <= pageSize
}"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showFirstLastButtons]="true"

2
libs/ui/src/lib/activity-type/activity-type.component.html

@ -1,6 +1,6 @@
<div
class="d-inline-flex p-1 activity-type-badge"
[ngClass]="{
[class]="{
buy: activityType === 'BUY',
dividend: activityType === 'DIVIDEND',
fee: activityType === 'FEE',

3
libs/ui/src/lib/activity-type/activity-type.component.ts

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -22,7 +21,7 @@ import { translate } from '../i18n';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, IonIcon],
imports: [IonIcon],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-activity-type',
styleUrls: ['./activity-type.component.scss'],

6
libs/ui/src/lib/benchmark/benchmark.component.html

@ -122,14 +122,14 @@
@if (isNumber(element?.performances?.allTimeHigh?.performancePercent)) {
<gf-value
class="d-inline-block justify-content-end"
[isPercent]="true"
[locale]="locale()"
[ngClass]="{
[class]="{
'text-danger':
element?.performances?.allTimeHigh?.performancePercent < 0,
'text-success':
element?.performances?.allTimeHigh?.performancePercent === 0
}"
[isPercent]="true"
[locale]="locale()"
[value]="element?.performances?.allTimeHigh?.performancePercent"
/>
}

2
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -11,7 +11,6 @@ import {
} from '@ghostfolio/common/interfaces';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -46,7 +45,6 @@ import { BenchmarkDetailDialogParams } from './benchmark-detail-dialog/interface
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfTrendIndicatorComponent,
GfValueComponent,
IonIcon,

2
libs/ui/src/lib/dialog-header/dialog-header.component.html

@ -1,7 +1,7 @@
<div class="d-flex" mat-dialog-title>
<span
class="flex-grow-1 text-truncate"
[ngClass]="{ 'text-center': position === 'center' }"
[class.text-center]="position === 'center'"
>{{ title }}</span
>
@if (deviceType !== 'mobile') {

3
libs/ui/src/lib/dialog-header/dialog-header.component.ts

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -15,7 +14,7 @@ import { close } from 'ionicons/icons';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'justify-content-center' },
imports: [CommonModule, IonIcon, MatButtonModule, MatDialogModule],
imports: [IonIcon, MatButtonModule, MatDialogModule],
selector: 'gf-dialog-header',
styleUrls: ['./dialog-header.component.scss'],
templateUrl: './dialog-header.component.html'

2
libs/ui/src/lib/entity-logo/entity-logo.component.html

@ -1,7 +1,7 @@
@if (src) {
<img
onerror="this.style.display = 'none'"
[ngClass]="{ large: size === 'large' }"
[class.large]="size === 'large'"
[src]="src"
[title]="tooltip ? tooltip : ''"
/>

2
libs/ui/src/lib/entity-logo/entity-logo.component.ts

@ -1,6 +1,5 @@
import { EntityLogoImageSourceService } from '@ghostfolio/ui/entity-logo/entity-logo-image-source.service';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -12,7 +11,6 @@ import { DataSource } from '@prisma/client';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-entity-logo',
styleUrls: ['./entity-logo.component.scss'],

2
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html

@ -6,7 +6,7 @@
@for (day of days; track day) {
<div
class="day"
[ngClass]="{
[class]="{
'cursor-pointer valid': isDateOfInterest(
`${itemByMonth.key}-${formatDay(day)}`
),

2
libs/ui/src/lib/logo/logo.component.html

@ -1,5 +1,5 @@
<span class="align-items-center d-flex"
><span class="d-inline-block logo" [ngClass]="{ 'mr-1': showLabel }"></span>
><span class="d-inline-block logo" [class.mr-1]="showLabel"></span>
@if (showLabel) {
<span class="label">{{ label ?? 'Ghostfolio' }}</span>
}

2
libs/ui/src/lib/logo/logo.component.ts

@ -1,4 +1,3 @@
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -9,7 +8,6 @@ import {
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-logo',
styleUrls: ['./logo.component.scss'],

4
libs/ui/src/lib/membership-card/membership-card.component.html

@ -1,5 +1,5 @@
<div class="card-wrapper position-relative" [class.hover-3d]="hover3d">
<div class="card-container" [ngClass]="{ premium: name === 'Premium' }">
<div class="card-container" [class.premium]="name === 'Premium'">
<a
class="card-item d-flex flex-column justify-content-between p-4"
[routerLink]="routerLinkPricing"
@ -12,7 +12,7 @@
<div class="d-flex justify-content-end">
<gf-logo
size="large"
[ngClass]="{ 'text-muted': name === 'Basic' }"
[class.text-muted]="name === 'Basic'"
[showLabel]="false"
/>
</div>

9
libs/ui/src/lib/membership-card/membership-card.component.ts

@ -1,6 +1,5 @@
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -19,13 +18,7 @@ import { GfLogoComponent } from '../logo';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfLogoComponent,
IonIcon,
MatButtonModule,
RouterModule
],
imports: [GfLogoComponent, IonIcon, MatButtonModule, RouterModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-membership-card',
styleUrls: ['./membership-card.component.scss'],

4
libs/ui/src/lib/toggle/toggle.component.html

@ -6,12 +6,12 @@
@for (option of options(); track option) {
<mat-radio-button
class="d-inline-flex"
[disabled]="isLoading()"
[ngClass]="{
[class]="{
'cursor-default': option.value === optionFormControl.value,
'cursor-pointer':
!isLoading() && option.value !== optionFormControl.value
}"
[disabled]="isLoading()"
[value]="option.value"
>{{ option.label }}</mat-radio-button
>

3
libs/ui/src/lib/toggle/toggle.component.ts

@ -1,6 +1,5 @@
import { ToggleOption } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -13,7 +12,7 @@ import { MatRadioModule } from '@angular/material/radio';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, MatRadioModule, ReactiveFormsModule],
imports: [MatRadioModule, ReactiveFormsModule],
selector: 'gf-toggle',
styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html'

6
libs/ui/src/lib/top-holdings/top-holdings.component.html

@ -120,7 +120,7 @@
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{ 'cursor-pointer': row.position }"
[class.cursor-pointer]="row.position"
(click)="onClickHolding(row.position)"
></tr>
<tr
@ -138,7 +138,7 @@
<tr
*matRowDef="let element; columns: displayedColumns"
mat-row
[ngClass]="{
[class]="{
'cursor-pointer': element.parents?.length > 0,
expanded: element.expand ?? false
}"
@ -150,7 +150,7 @@
*matRowDef="let row; columns: ['expandedDetail']"
class="holding-detail"
mat-row
[ngClass]="{ 'd-none': !row.parents?.length }"
[class.d-none]="!row.parents?.length"
></tr>
</table>
</div>

2
libs/ui/src/lib/top-holdings/top-holdings.component.ts

@ -12,7 +12,6 @@ import {
transition,
trigger
} from '@angular/animations';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@ -44,7 +43,6 @@ import { GfValueComponent } from '../value/value.component';
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfSymbolPipe,
GfValueComponent,
MatButtonModule,

4
libs/ui/src/lib/trend-indicator/trend-indicator.component.html

@ -16,7 +16,7 @@
<ion-icon
class="text-danger"
name="arrow-down-circle-outline"
[ngClass]="{ 'rotate-45-down': value > -0.01 }"
[class.rotate-45-down]="value > -0.01"
[size]="size"
/>
} @else if (value > -0.0005 && value < 0.0005) {
@ -29,7 +29,7 @@
<ion-icon
class="text-success"
name="arrow-up-circle-outline"
[ngClass]="{ 'rotate-45-up': value < 0.01 }"
[class.rotate-45-up]="value < 0.01"
[size]="size"
/>
}

6
libs/ui/src/lib/value/value.component.html

@ -27,7 +27,7 @@
@if (value || value === 0 || value === null) {
<div
class="align-items-center d-flex"
[ngClass]="position === 'end' ? 'justify-content-end' : ''"
[class.justify-content-end]="position === 'end'"
>
@if (isNumber || value === null) {
@if (colorizeSign && !useAbsoluteValue) {
@ -40,7 +40,7 @@
}
<div
class="mb-0 value"
[ngClass]="{
[class]="{
'font-weight-bold h2': size === 'large',
h4: size === 'medium'
}"
@ -60,7 +60,7 @@
@if (isString) {
<div
class="mb-0 text-truncate value"
[ngClass]="{
[class]="{
'font-weight-bold h2': size === 'large',
h4: size === 'medium'
}"

1598
package-lock.json

File diff suppressed because it is too large

38
package.json

@ -66,13 +66,13 @@
"@angular/platform-browser-dynamic": "21.2.7",
"@angular/router": "21.2.7",
"@angular/service-worker": "21.2.7",
"@bull-board/api": "6.20.3",
"@bull-board/express": "6.20.3",
"@bull-board/nestjs": "6.20.3",
"@bull-board/api": "7.0.0",
"@bull-board/express": "7.0.0",
"@bull-board/nestjs": "7.0.0",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.5",
"@ionic/angular": "8.8.1",
"@ionic/angular": "8.8.5",
"@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.0",
@ -85,12 +85,12 @@
"@nestjs/platform-express": "11.1.19",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@openrouter/ai-sdk-provider": "0.7.2",
"@openrouter/ai-sdk-provider": "2.9.0",
"@prisma/adapter-pg": "7.7.0",
"@prisma/client": "7.7.0",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"ai": "4.3.16",
"ai": "6.0.174",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
"bootstrap": "4.6.2",
@ -105,7 +105,7 @@
"class-validator": "0.15.1",
"color": "5.0.3",
"cookie-parser": "1.4.7",
"countries-and-timezones": "3.8.0",
"countries-and-timezones": "3.9.0",
"countries-list": "3.3.0",
"countup.js": "2.10.0",
"date-fns": "4.1.0",
@ -113,7 +113,7 @@
"dotenv-expand": "12.0.3",
"envalid": "8.1.1",
"fast-redact": "3.5.0",
"fuse.js": "7.1.0",
"fuse.js": "7.3.0",
"google-spreadsheet": "3.2.0",
"helmet": "7.0.0",
"http-status-codes": "2.3.0",
@ -158,16 +158,16 @@
"@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.19",
"@nx/angular": "22.6.5",
"@nx/eslint-plugin": "22.6.5",
"@nx/jest": "22.6.5",
"@nx/js": "22.6.5",
"@nx/module-federation": "22.6.5",
"@nx/nest": "22.6.5",
"@nx/node": "22.6.5",
"@nx/storybook": "22.6.5",
"@nx/web": "22.6.5",
"@nx/workspace": "22.6.5",
"@nx/angular": "22.7.1",
"@nx/eslint-plugin": "22.7.1",
"@nx/jest": "22.7.1",
"@nx/js": "22.7.1",
"@nx/module-federation": "22.7.1",
"@nx/nest": "22.7.1",
"@nx/node": "22.7.1",
"@nx/storybook": "22.7.1",
"@nx/web": "22.7.1",
"@nx/workspace": "22.7.1",
"@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10",
"@storybook/addon-themes": "10.1.10",
@ -194,7 +194,7 @@
"jest": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0",
"nx": "22.6.5",
"nx": "22.7.1",
"prettier": "3.8.3",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.7.0",

Loading…
Cancel
Save