diff --git a/CHANGELOG.md b/CHANGELOG.md index 40cb4cc6..25c69a62 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 ### Added - Added a connection timeout to the environment variable `DATABASE_URL` +- Introduced the _Open Startup_ (`/open`) page with aggregated key metrics including uptime ### Fixed diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index f825418e..4da32966 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -7,6 +7,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { + PROPERTY_BETTER_UPTIME_MONITOR_ID, PROPERTY_COUNTRIES_OF_SUBSCRIBERS, PROPERTY_DEMO_USER_ID, PROPERTY_IS_READ_ONLY_MODE, @@ -115,19 +116,28 @@ export class InfoService { globalPermissions.push(permissions.createUserAccount); } + const [benchmarks, demoAuthToken, statistics, subscriptions, tags] = + await Promise.all([ + this.benchmarkService.getBenchmarkAssetProfiles(), + this.getDemoAuthToken(), + this.getStatistics(), + this.getSubscriptions(), + this.tagService.get() + ]); + return { ...info, + benchmarks, + demoAuthToken, globalPermissions, isReadOnlyMode, platforms, + statistics, + subscriptions, systemMessage, + tags, baseCurrency: this.configurationService.get('BASE_CURRENCY'), - benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(), - currencies: this.exchangeRateDataService.getCurrencies(), - demoAuthToken: await this.getDemoAuthToken(), - statistics: await this.getStatistics(), - subscriptions: await this.getSubscriptions(), - tags: await this.tagService.get() + currencies: this.exchangeRateDataService.getCurrencies() }; } @@ -291,6 +301,7 @@ export class InfoService { const gitHubContributors = await this.countGitHubContributors(); const gitHubStargazers = await this.countGitHubStargazers(); const slackCommunityUsers = await this.countSlackCommunityUsers(); + const uptime = await this.getUptime(); statistics = { activeUsers1d, @@ -299,7 +310,8 @@ export class InfoService { gitHubContributors, gitHubStargazers, newUsers30d, - slackCommunityUsers + slackCommunityUsers, + uptime }; await this.redisCacheService.set( @@ -323,4 +335,33 @@ export class InfoService { return JSON.parse(stripeConfig.value); } + + private async getUptime(): Promise { + { + try { + const monitorId = (await this.propertyService.getByKey( + PROPERTY_BETTER_UPTIME_MONITOR_ID + )) as string; + + const get = bent( + `https://betteruptime.com/api/v2/monitors/${monitorId}/sla`, + 'GET', + 'json', + 200, + { + Authorization: `Bearer ${this.configurationService.get( + 'BETTER_UPTIME_API_KEY' + )}` + } + ); + + const { data } = await get(); + return data.attributes.availability / 100; + } catch (error) { + Logger.error(error, 'InfoService'); + + return undefined; + } + } + } } diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index fa1255fc..c5a46890 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -15,6 +15,7 @@ export class ConfigurationService { choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'], default: 'USD' }), + BETTER_UPTIME_API_KEY: str({ default: '' }), CACHE_TTL: num({ default: 1 }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 84130956..1256f0f2 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors { ACCESS_TOKEN_SALT: string; ALPHA_VANTAGE_API_KEY: string; BASE_CURRENCY: string; + BETTER_UPTIME_API_KEY: string; CACHE_TTL: number; DATA_SOURCE_EXCHANGE_RATES: string; DATA_SOURCE_IMPORT: string; diff --git a/apps/client/src/app/app-routing.module.ts b/apps/client/src/app/app-routing.module.ts index dd5ba3c5..0f6acd87 100644 --- a/apps/client/src/app/app-routing.module.ts +++ b/apps/client/src/app/app-routing.module.ts @@ -166,6 +166,11 @@ const routes: Routes = [ (m) => m.MarketsPageModule ) }, + { + path: 'open', + loadChildren: () => + import('./pages/open/open-page.module').then((m) => m.OpenPageModule) + }, { path: 'p', loadChildren: () => diff --git a/apps/client/src/app/core/auth.guard.ts b/apps/client/src/app/core/auth.guard.ts index c193017c..ecf7d042 100644 --- a/apps/client/src/app/core/auth.guard.ts +++ b/apps/client/src/app/core/auth.guard.ts @@ -24,6 +24,7 @@ export class AuthGuard implements CanActivate { '/faq', '/features', '/markets', + '/open', '/p', '/pricing', '/register', diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index e14ef60c..5f2b33a9 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { environment } from '@ghostfolio/api/environments/environment'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; - @Component({ host: { class: 'page' }, selector: 'gf-about-page', @@ -16,6 +16,7 @@ import { environment } from '../../../environments/environment'; templateUrl: './about-page.html' }) export class AboutPageComponent implements OnDestroy, OnInit { + public defaultLanguageCode = DEFAULT_LANGUAGE_CODE; public hasPermissionForBlog: boolean; public hasPermissionForStatistics: boolean; public hasPermissionForSubscription: boolean; diff --git a/apps/client/src/app/pages/about/about-page.html b/apps/client/src/app/pages/about/about-page.html index 3dcf62c3..ab8bd36c 100644 --- a/apps/client/src/app/pages/about/about-page.html +++ b/apps/client/src/app/pages/about/about-page.html @@ -6,9 +6,12 @@

Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make - solid, data-driven investment decisions. The source code is fully - available as open source software (OSS). The project has been - initiated by + solid, data-driven investment decisions. We share aggregated + key metrics + of our platform’s performance and the source code is fully available + as open source software (OSS). The project has been initiated by Thomas Kaul diff --git a/apps/client/src/app/pages/open/open-page-routing.module.ts b/apps/client/src/app/pages/open/open-page-routing.module.ts new file mode 100644 index 00000000..80cb3fef --- /dev/null +++ b/apps/client/src/app/pages/open/open-page-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; + +import { OpenPageComponent } from './open-page.component'; + +const routes: Routes = [ + { + canActivate: [AuthGuard], + component: OpenPageComponent, + path: '', + title: $localize`Open Startup` + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class OpenPageRoutingModule {} diff --git a/apps/client/src/app/pages/open/open-page.component.ts b/apps/client/src/app/pages/open/open-page.component.ts new file mode 100644 index 00000000..554ab3be --- /dev/null +++ b/apps/client/src/app/pages/open/open-page.component.ts @@ -0,0 +1,29 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; +import { Subject } from 'rxjs'; + +@Component({ + host: { class: 'page' }, + selector: 'gf-open-page', + styleUrls: ['./open-page.scss'], + templateUrl: './open-page.html' +}) +export class OpenPageComponent implements OnDestroy, OnInit { + public statistics: Statistics; + + private unsubscribeSubject = new Subject(); + + public constructor(private dataService: DataService) { + const { statistics } = this.dataService.fetchInfo(); + + this.statistics = statistics; + } + + public ngOnInit() {} + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/pages/open/open-page.html b/apps/client/src/app/pages/open/open-page.html new file mode 100644 index 00000000..2de58842 --- /dev/null +++ b/apps/client/src/app/pages/open/open-page.html @@ -0,0 +1,111 @@ +

+
+
+

Open Startup

+
+

+ At Ghostfolio, transparency is at the core of our values. We openly + share aggregated key metrics of our platform’s performance and publish + the source code as + open source software + (OSS). +

+
+
+
+ +
+
+ + +
+
+ Active Users +
+
+ New Users +
+
+ Active Users +
+ + + + + +
+
+
+
+
+
diff --git a/apps/client/src/app/pages/open/open-page.module.ts b/apps/client/src/app/pages/open/open-page.module.ts new file mode 100644 index 00000000..6a9c988d --- /dev/null +++ b/apps/client/src/app/pages/open/open-page.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { GfValueModule } from '@ghostfolio/ui/value'; + +import { OpenPageRoutingModule } from './open-page-routing.module'; +import { OpenPageComponent } from './open-page.component'; + +@NgModule({ + declarations: [OpenPageComponent], + imports: [CommonModule, GfValueModule, MatCardModule, OpenPageRoutingModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class OpenPageModule {} diff --git a/apps/client/src/app/pages/open/open-page.scss b/apps/client/src/app/pages/open/open-page.scss new file mode 100644 index 00000000..e58d9f23 --- /dev/null +++ b/apps/client/src/app/pages/open/open-page.scss @@ -0,0 +1,19 @@ +:host { + color: rgb(var(--dark-primary-text)); + display: block; + + .intro-container { + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: 500; + + &:hover { + color: rgba(var(--palette-primary-300), 1); + } + } + } +} + +:host-context(.is-dark-theme) { + color: rgb(var(--light-primary-text)); +} diff --git a/apps/client/src/assets/sitemap.xml b/apps/client/src/assets/sitemap.xml index 110254e0..56e7872c 100644 --- a/apps/client/src/assets/sitemap.xml +++ b/apps/client/src/assets/sitemap.xml @@ -6,102 +6,106 @@ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> https://ghostfol.io - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/de/blog - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/de/pricing - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/about - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/about/changelog - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2021/07/hello-ghostfolio - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/08/500-stars-on-github - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022 - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/11/black-friday-2022 - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/demo - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/faq - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/features - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/markets - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 + + + https://ghostfol.io/en/open + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/pricing - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/register - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 https://ghostfol.io/en/resources - 2023-03-25T00:00:00+00:00 + 2023-05-15T00:00:00+00:00 diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 4b8a6d08..461d801e 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -70,6 +70,7 @@ export const HEADER_KEY_TOKEN = 'Authorization'; export const MAX_CHART_ITEMS = 365; export const PROPERTY_BENCHMARKS = 'BENCHMARKS'; +export const PROPERTY_BETTER_UPTIME_MONITOR_ID = 'BETTER_UPTIME_MONITOR_ID'; export const PROPERTY_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS'; export const PROPERTY_COUPONS = 'COUPONS'; export const PROPERTY_CURRENCIES = 'CURRENCIES'; diff --git a/libs/common/src/lib/interfaces/statistics.interface.ts b/libs/common/src/lib/interfaces/statistics.interface.ts index f3952942..2852d34a 100644 --- a/libs/common/src/lib/interfaces/statistics.interface.ts +++ b/libs/common/src/lib/interfaces/statistics.interface.ts @@ -6,4 +6,5 @@ export interface Statistics { gitHubStargazers: number; newUsers30d: number; slackCommunityUsers: string; + uptime: number; }