Feature/setup open startup page (#1967)

* Setup Open Startup page

* Update changelog
This commit is contained in:
Thomas Kaul 2023-05-18 12:31:36 +02:00 committed by GitHub
parent 302339e1cd
commit edd690850c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 290 additions and 37 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a connection timeout to the environment variable `DATABASE_URL` - Added a connection timeout to the environment variable `DATABASE_URL`
- Introduced the _Open Startup_ (`/open`) page with aggregated key metrics including uptime
### Fixed ### Fixed

View File

@ -7,6 +7,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
PROPERTY_BETTER_UPTIME_MONITOR_ID,
PROPERTY_COUNTRIES_OF_SUBSCRIBERS, PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
PROPERTY_DEMO_USER_ID, PROPERTY_DEMO_USER_ID,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
@ -115,19 +116,28 @@ export class InfoService {
globalPermissions.push(permissions.createUserAccount); 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 { return {
...info, ...info,
benchmarks,
demoAuthToken,
globalPermissions, globalPermissions,
isReadOnlyMode, isReadOnlyMode,
platforms, platforms,
statistics,
subscriptions,
systemMessage, systemMessage,
tags,
baseCurrency: this.configurationService.get('BASE_CURRENCY'), baseCurrency: this.configurationService.get('BASE_CURRENCY'),
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(), currencies: this.exchangeRateDataService.getCurrencies()
currencies: this.exchangeRateDataService.getCurrencies(),
demoAuthToken: await this.getDemoAuthToken(),
statistics: await this.getStatistics(),
subscriptions: await this.getSubscriptions(),
tags: await this.tagService.get()
}; };
} }
@ -291,6 +301,7 @@ export class InfoService {
const gitHubContributors = await this.countGitHubContributors(); const gitHubContributors = await this.countGitHubContributors();
const gitHubStargazers = await this.countGitHubStargazers(); const gitHubStargazers = await this.countGitHubStargazers();
const slackCommunityUsers = await this.countSlackCommunityUsers(); const slackCommunityUsers = await this.countSlackCommunityUsers();
const uptime = await this.getUptime();
statistics = { statistics = {
activeUsers1d, activeUsers1d,
@ -299,7 +310,8 @@ export class InfoService {
gitHubContributors, gitHubContributors,
gitHubStargazers, gitHubStargazers,
newUsers30d, newUsers30d,
slackCommunityUsers slackCommunityUsers,
uptime
}; };
await this.redisCacheService.set( await this.redisCacheService.set(
@ -323,4 +335,33 @@ export class InfoService {
return JSON.parse(stripeConfig.value); return JSON.parse(stripeConfig.value);
} }
private async getUptime(): Promise<number> {
{
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;
}
}
}
} }

View File

@ -15,6 +15,7 @@ export class ConfigurationService {
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'], choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
default: 'USD' default: 'USD'
}), }),
BETTER_UPTIME_API_KEY: str({ default: '' }),
CACHE_TTL: num({ default: 1 }), CACHE_TTL: num({ default: 1 }),
DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }),
DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }),

View File

@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors {
ACCESS_TOKEN_SALT: string; ACCESS_TOKEN_SALT: string;
ALPHA_VANTAGE_API_KEY: string; ALPHA_VANTAGE_API_KEY: string;
BASE_CURRENCY: string; BASE_CURRENCY: string;
BETTER_UPTIME_API_KEY: string;
CACHE_TTL: number; CACHE_TTL: number;
DATA_SOURCE_EXCHANGE_RATES: string; DATA_SOURCE_EXCHANGE_RATES: string;
DATA_SOURCE_IMPORT: string; DATA_SOURCE_IMPORT: string;

View File

@ -166,6 +166,11 @@ const routes: Routes = [
(m) => m.MarketsPageModule (m) => m.MarketsPageModule
) )
}, },
{
path: 'open',
loadChildren: () =>
import('./pages/open/open-page.module').then((m) => m.OpenPageModule)
},
{ {
path: 'p', path: 'p',
loadChildren: () => loadChildren: () =>

View File

@ -24,6 +24,7 @@ export class AuthGuard implements CanActivate {
'/faq', '/faq',
'/features', '/features',
'/markets', '/markets',
'/open',
'/p', '/p',
'/pricing', '/pricing',
'/register', '/register',

View File

@ -1,14 +1,14 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { environment } from '@ghostfolio/api/environments/environment';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.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 { User } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
selector: 'gf-about-page', selector: 'gf-about-page',
@ -16,6 +16,7 @@ import { environment } from '../../../environments/environment';
templateUrl: './about-page.html' templateUrl: './about-page.html'
}) })
export class AboutPageComponent implements OnDestroy, OnInit { export class AboutPageComponent implements OnDestroy, OnInit {
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
public hasPermissionForBlog: boolean; public hasPermissionForBlog: boolean;
public hasPermissionForStatistics: boolean; public hasPermissionForStatistics: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;

View File

@ -6,9 +6,12 @@
<p> <p>
Ghostfolio is a lightweight wealth management application for Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and make individuals to keep track of stocks, ETFs or cryptocurrencies and make
solid, data-driven investment decisions. The source code is fully solid, data-driven investment decisions. We share aggregated
available as open source software (OSS). The project has been <a href="https://ghostfol.io/{{ defaultLanguageCode }}/open"
initiated by >key metrics</a
>
of our platforms performance and the source code is fully available
as open source software (OSS). The project has been initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul" <a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a >Thomas Kaul</a
> >

View File

@ -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 {}

View File

@ -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<void>();
public constructor(private dataService: DataService) {
const { statistics } = this.dataService.fetchInfo();
this.statistics = statistics;
}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,111 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center">Open Startup</h3>
<div class="intro-container">
<p>
At Ghostfolio, transparency is at the core of our values. We openly
share aggregated key metrics of our platforms performance and publish
the source code as
<a
href="https://github.com/ghostfolio/ghostfolio"
title="Contributors to Ghostfolio"
>open source software</a
>
(OSS).
</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<mat-card appearance="outlined">
<mat-card-content>
<div class="row">
<div class="col-xs-12 col-md-4 my-2">
<gf-value
size="large"
subLabel="(Last 24 hours)"
[value]="statistics?.activeUsers1d ?? '-'"
>Active Users</gf-value
>
</div>
<div class="col-xs-12 col-md-4 my-2">
<gf-value
size="large"
subLabel="(Last 30 days)"
[value]="statistics?.newUsers30d ?? '-'"
>New Users</gf-value
>
</div>
<div class="col-xs-12 col-md-4 my-2">
<gf-value
size="large"
subLabel="(Last 30 days)"
[value]="statistics?.activeUsers30d ?? '-'"
>Active Users</gf-value
>
</div>
<div class="col-xs-12 col-md-4 my-2">
<a class="d-block" href="https://ghostfolio.slack.com">
<gf-value
size="large"
[value]="statistics?.slackCommunityUsers ?? '-'"
>Users in Slack community</gf-value
>
</a>
</div>
<div class="col-xs-12 col-md-4 my-2">
<a
class="d-block"
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
>
<gf-value
size="large"
[value]="statistics?.gitHubContributors ?? '-'"
>Contributors on GitHub</gf-value
>
</a>
</div>
<div class="col-xs-12 col-md-4 my-2">
<a
class="d-block"
href="https://github.com/ghostfolio/ghostfolio/stargazers"
>
<gf-value
size="large"
[value]="statistics?.gitHubStargazers ?? '-'"
>Stars on GitHub</gf-value
>
</a>
</div>
<div class="col-xs-12 col-md-4 my-2">
<a
class="d-block"
href="https://hub.docker.com/r/ghostfolio/ghostfolio"
>
<gf-value
size="large"
[value]="statistics?.dockerHubPulls ?? '-'"
>Pulls on Docker Hub</gf-value
>
</a>
</div>
<div class="col-xs-12 col-md-4 my-2">
<a class="d-block" href="https://status.ghostfol.io">
<gf-value
size="large"
[isPercent]="true"
[precision]="2"
[value]="statistics?.uptime ?? '-'"
>Uptime</gf-value
>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

View File

@ -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 {}

View File

@ -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));
}

View File

@ -6,102 +6,106 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url> <url>
<loc>https://ghostfol.io</loc> <loc>https://ghostfol.io</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog</loc> <loc>https://ghostfol.io/de/blog</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc> <loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc> <loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/de/pricing</loc> <loc>https://ghostfol.io/de/pricing</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about</loc> <loc>https://ghostfol.io/en/about</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/about/changelog</loc> <loc>https://ghostfol.io/en/about/changelog</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog</loc> <loc>https://ghostfol.io/en/blog</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc> <loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc> <loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc> <loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc> <loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc> <loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc> <loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc> <loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc> <loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc> <loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc> <loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/demo</loc> <loc>https://ghostfol.io/en/demo</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/faq</loc> <loc>https://ghostfol.io/en/faq</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/features</loc> <loc>https://ghostfol.io/en/features</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/markets</loc> <loc>https://ghostfol.io/en/markets</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/open</loc>
<lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/pricing</loc> <loc>https://ghostfol.io/en/pricing</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/register</loc> <loc>https://ghostfol.io/en/register</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
<url> <url>
<loc>https://ghostfol.io/en/resources</loc> <loc>https://ghostfol.io/en/resources</loc>
<lastmod>2023-03-25T00:00:00+00:00</lastmod> <lastmod>2023-05-15T00:00:00+00:00</lastmod>
</url> </url>
</urlset> </urlset>

View File

@ -70,6 +70,7 @@ export const HEADER_KEY_TOKEN = 'Authorization';
export const MAX_CHART_ITEMS = 365; export const MAX_CHART_ITEMS = 365;
export const PROPERTY_BENCHMARKS = 'BENCHMARKS'; 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_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS';
export const PROPERTY_COUPONS = 'COUPONS'; export const PROPERTY_COUPONS = 'COUPONS';
export const PROPERTY_CURRENCIES = 'CURRENCIES'; export const PROPERTY_CURRENCIES = 'CURRENCIES';

View File

@ -6,4 +6,5 @@ export interface Statistics {
gitHubStargazers: number; gitHubStargazers: number;
newUsers30d: number; newUsers30d: number;
slackCommunityUsers: string; slackCommunityUsers: string;
uptime: number;
} }