Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
0de28d733e | |||
3b2f13850c | |||
0cc42ffd7c | |||
3ccb812ac3 | |||
0a8549db3e | |||
c95e90ff31 | |||
b59af0d864 | |||
408bdbd187 | |||
a3bfa46fb0 | |||
8cb1b3f925 | |||
15c650f951 | |||
c198bd78da | |||
35963580bc | |||
cf2c5bad02 | |||
f332aea9b4 | |||
7a9fd18407 | |||
ca08d3154a | |||
01d4ae8757 | |||
43ce2786c1 | |||
de2092c4d2 |
3
.github/workflows/build-code.yml
vendored
3
.github/workflows/build-code.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Build code
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@ -13,6 +14,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node_version }}
|
||||
uses: actions/setup-node@v3
|
||||
|
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
with:
|
||||
images: ghostfolio/ghostfolio
|
||||
tags: |
|
||||
type=raw,value=linux-arm64-{{version}}
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@ -41,7 +41,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.output.labels }}
|
||||
|
30
.travis.yml
30
.travis.yml
@ -1,30 +0,0 @@
|
||||
language: node_js
|
||||
git:
|
||||
depth: false
|
||||
node_js:
|
||||
- 16
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache: yarn
|
||||
|
||||
if: (type = pull_request) OR (tag IS present)
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: Install dependencies
|
||||
if: type = pull_request
|
||||
script: yarn --frozen-lockfile
|
||||
- stage: Check formatting
|
||||
if: type = pull_request
|
||||
script: yarn format:check
|
||||
- stage: Execute tests
|
||||
if: type = pull_request
|
||||
script: yarn test
|
||||
- stage: Build application
|
||||
if: type = pull_request
|
||||
script: yarn build:all
|
||||
- stage: Build and publish docker image
|
||||
if: tag IS present
|
||||
script: ./publish-docker-image.sh
|
39
CHANGELOG.md
39
CHANGELOG.md
@ -5,7 +5,44 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.184.1 - 28.08.2022
|
||||
## 1.187.0 - 03.09.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Supported units in the line chart component
|
||||
- Added a new chart calculation engine (experimental)
|
||||
|
||||
## 1.186.2 - 03.09.2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Decreased the rate limiter duration of queue jobs from 5 to 4 seconds
|
||||
- Removed the alias from the `User` database schema
|
||||
- Upgraded `angular` from version `14.1.0` to `14.2.0`
|
||||
- Upgraded `Nx` from version `14.5.1` to `14.6.4`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the environment variables `REDIS_HOST`, `REDIS_PASSWORD` and `REDIS_PORT` in the Redis configuration
|
||||
- Handled errors in the portfolio calculation if there is no internet connection
|
||||
- Fixed the _GitHub_ contributors count on the about page
|
||||
|
||||
## 1.185.0 - 30.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added a skeleton loader to the market mood component in the markets overview
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the build pipeline from _Travis_ to _GitHub Actions_
|
||||
- Increased the caching of the benchmarks
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disabled the language selector for the demo user
|
||||
|
||||
## 1.184.2 - 28.08.2022
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -17,8 +17,6 @@
|
||||
<p>
|
||||
<a href="#contributing">
|
||||
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
|
||||
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
|
||||
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
|
||||
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/></a>
|
||||
</p>
|
||||
@ -81,6 +79,8 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
||||
|
||||
## Self-hosting
|
||||
|
||||
We provide official container images hosted on [Docker Hub](https://hub.docker.com/r/ghostfolio/ghostfolio) for `linux/amd64` and `linux/arm64`.
|
||||
|
||||
### Supported Environment Variables
|
||||
|
||||
| Name | Default Value | Description |
|
||||
@ -94,9 +94,9 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
||||
| `REDIS_HOST` | `localhost` | The host where _Redis_ is running |
|
||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
||||
| `REDIS_PORT` | `6379` | The port where _Redis_ is running |
|
||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'api',
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class WebAuthService {
|
||||
rpName: 'Ghostfolio',
|
||||
rpID: this.rpID,
|
||||
userID: user.id,
|
||||
userName: user.alias,
|
||||
userName: '',
|
||||
timeout: 60000,
|
||||
attestationType: 'indirect',
|
||||
authenticatorSelection: {
|
||||
|
@ -1,30 +1,20 @@
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { BenchmarkResponse } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import { BenchmarkService } from './benchmark.service';
|
||||
|
||||
@Controller('benchmark')
|
||||
export class BenchmarkController {
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly propertyService: PropertyService
|
||||
) {}
|
||||
public constructor(private readonly benchmarkService: BenchmarkService) {}
|
||||
|
||||
@Get()
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
|
||||
return {
|
||||
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets)
|
||||
benchmarks: await this.benchmarkService.getBenchmarks()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Big from 'big.js';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class BenchmarkService {
|
||||
@ -13,15 +16,17 @@ export class BenchmarkService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public async getBenchmarks(
|
||||
benchmarkAssets: UniqueAsset[]
|
||||
): Promise<BenchmarkResponse['benchmarks']> {
|
||||
public async getBenchmarks({ useCache = true } = {}): Promise<
|
||||
BenchmarkResponse['benchmarks']
|
||||
> {
|
||||
let benchmarks: BenchmarkResponse['benchmarks'];
|
||||
|
||||
if (useCache) {
|
||||
try {
|
||||
benchmarks = JSON.parse(
|
||||
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
|
||||
@ -31,7 +36,12 @@ export class BenchmarkService {
|
||||
return benchmarks;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
const promises: Promise<number>[] = [];
|
||||
|
||||
const [quotes, assetProfiles] = await Promise.all([
|
||||
@ -76,7 +86,8 @@ export class BenchmarkService {
|
||||
|
||||
await this.redisCacheService.set(
|
||||
this.CACHE_KEY_BENCHMARKS,
|
||||
JSON.stringify(benchmarks)
|
||||
JSON.stringify(benchmarks),
|
||||
ms('4 hours') / 1000
|
||||
);
|
||||
|
||||
return benchmarks;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
@ -13,7 +12,10 @@ import {
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
ghostfolioFearAndGreedIndexDataSource
|
||||
} from '@ghostfolio/common/config';
|
||||
import { encodeDataSource } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
encodeDataSource,
|
||||
extractNumberFromString
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||
@ -21,6 +23,7 @@ import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bent from 'bent';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
@ -30,7 +33,6 @@ export class InfoService {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly dataGatheringService: DataGatheringService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
@ -143,17 +145,21 @@ export class InfoService {
|
||||
private async countGitHubContributors(): Promise<number> {
|
||||
try {
|
||||
const get = bent(
|
||||
`https://api.github.com/repos/ghostfolio/ghostfolio/contributors`,
|
||||
'https://github.com/ghostfolio/ghostfolio',
|
||||
'GET',
|
||||
'json',
|
||||
'string',
|
||||
200,
|
||||
{
|
||||
'User-Agent': 'request'
|
||||
}
|
||||
{}
|
||||
);
|
||||
|
||||
const contributors = await get();
|
||||
return contributors?.length;
|
||||
const html = await get();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
return extractNumberFromString(
|
||||
$(
|
||||
`a[href="/ghostfolio/ghostfolio/graphs/contributors"] .Counter`
|
||||
).text()
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
isBefore,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
isWithinInterval,
|
||||
max,
|
||||
min,
|
||||
set
|
||||
@ -167,13 +168,21 @@ export class PortfolioCalculator {
|
||||
this.transactionPoints = transactionPoints;
|
||||
}
|
||||
|
||||
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
|
||||
if (!this.transactionPoints?.length) {
|
||||
public async getCurrentPositions(
|
||||
start: Date,
|
||||
end = new Date(Date.now())
|
||||
): Promise<CurrentPositions> {
|
||||
const transactionPointsBeforeEndDate =
|
||||
this.transactionPoints?.filter((transactionPoint) => {
|
||||
return isBefore(parseDate(transactionPoint.date), end);
|
||||
}) ?? [];
|
||||
|
||||
if (!transactionPointsBeforeEndDate.length) {
|
||||
return {
|
||||
currentValue: new Big(0),
|
||||
hasErrors: false,
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
positions: [],
|
||||
@ -182,39 +191,38 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
const lastTransactionPoint =
|
||||
this.transactionPoints[this.transactionPoints.length - 1];
|
||||
|
||||
// use Date.now() to use the mock for today
|
||||
const today = new Date(Date.now());
|
||||
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
|
||||
|
||||
let firstTransactionPoint: TransactionPoint = null;
|
||||
let firstIndex = this.transactionPoints.length;
|
||||
let firstIndex = transactionPointsBeforeEndDate.length;
|
||||
const dates = [];
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
|
||||
dates.push(resetHours(start));
|
||||
for (const item of this.transactionPoints[firstIndex - 1].items) {
|
||||
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource: item.dataSource,
|
||||
symbol: item.symbol
|
||||
});
|
||||
currencies[item.symbol] = item.currency;
|
||||
}
|
||||
for (let i = 0; i < this.transactionPoints.length; i++) {
|
||||
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
|
||||
if (
|
||||
!isBefore(parseDate(this.transactionPoints[i].date), start) &&
|
||||
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
|
||||
firstTransactionPoint === null
|
||||
) {
|
||||
firstTransactionPoint = this.transactionPoints[i];
|
||||
firstTransactionPoint = transactionPointsBeforeEndDate[i];
|
||||
firstIndex = i;
|
||||
}
|
||||
if (firstTransactionPoint !== null) {
|
||||
dates.push(resetHours(parseDate(this.transactionPoints[i].date)));
|
||||
dates.push(
|
||||
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dates.push(resetHours(today));
|
||||
dates.push(resetHours(end));
|
||||
|
||||
const marketSymbols = await this.currentRateService.getValues({
|
||||
currencies,
|
||||
@ -241,7 +249,7 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const todayString = format(today, DATE_FORMAT);
|
||||
const endDateString = format(end, DATE_FORMAT);
|
||||
|
||||
if (firstIndex > 0) {
|
||||
firstIndex--;
|
||||
@ -254,7 +262,7 @@ export class PortfolioCalculator {
|
||||
const errors: ResponseError['errors'] = [];
|
||||
|
||||
for (const item of lastTransactionPoint.items) {
|
||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
||||
const marketValue = marketSymbolMap[endDateString]?.[item.symbol];
|
||||
|
||||
const {
|
||||
grossPerformance,
|
||||
@ -264,6 +272,7 @@ export class PortfolioCalculator {
|
||||
netPerformance,
|
||||
netPerformancePercentage
|
||||
} = this.getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
symbol: item.symbol
|
||||
@ -432,10 +441,15 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
let minNetPerformance = new Big(0);
|
||||
let maxNetPerformance = new Big(0);
|
||||
|
||||
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
|
||||
timelinePeriodPromises
|
||||
);
|
||||
const minNetPerformance = timelineInfoInterfaces
|
||||
|
||||
try {
|
||||
minNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.minNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((minPerformance, current) => {
|
||||
@ -446,7 +460,7 @@ export class PortfolioCalculator {
|
||||
}
|
||||
});
|
||||
|
||||
const maxNetPerformance = timelineInfoInterfaces
|
||||
maxNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.maxNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((maxPerformance, current) => {
|
||||
@ -456,6 +470,7 @@ export class PortfolioCalculator {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const timelinePeriods = timelineInfoInterfaces.map(
|
||||
(timelineInfo) => timelineInfo.timelinePeriods
|
||||
@ -694,10 +709,12 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
private getSymbolMetrics({
|
||||
end,
|
||||
marketSymbolMap,
|
||||
start,
|
||||
symbol
|
||||
}: {
|
||||
end: Date;
|
||||
marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
};
|
||||
@ -720,13 +737,12 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
const dateOfFirstTransaction = new Date(first(orders).date);
|
||||
const endDate = new Date(Date.now());
|
||||
|
||||
const unitPriceAtStartDate =
|
||||
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
|
||||
|
||||
const unitPriceAtEndDate =
|
||||
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol];
|
||||
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
|
||||
|
||||
if (
|
||||
!unitPriceAtEndDate ||
|
||||
@ -779,7 +795,7 @@ export class PortfolioCalculator {
|
||||
orders.push({
|
||||
symbol,
|
||||
currency: null,
|
||||
date: format(endDate, DATE_FORMAT),
|
||||
date: format(end, DATE_FORMAT),
|
||||
dataSource: null,
|
||||
fee: new Big(0),
|
||||
itemType: 'end',
|
||||
|
@ -35,7 +35,8 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
UseInterceptors,
|
||||
Version
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
@ -110,6 +111,26 @@ export class PortfolioController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get('chart')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Version('2')
|
||||
public async getChartV2(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range
|
||||
): Promise<PortfolioChart> {
|
||||
const historicalDataContainer = await this.portfolioService.getChartV2(
|
||||
impersonationId,
|
||||
range
|
||||
);
|
||||
|
||||
return {
|
||||
chart: historicalDataContainer.items,
|
||||
hasError: false,
|
||||
isAllTimeHigh: false,
|
||||
isAllTimeLow: false
|
||||
};
|
||||
}
|
||||
|
||||
@Get('details')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
|
@ -57,6 +57,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
addDays,
|
||||
differenceInDays,
|
||||
endOfToday,
|
||||
format,
|
||||
@ -71,7 +72,7 @@ import {
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash';
|
||||
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataContainer,
|
||||
@ -85,6 +86,7 @@ const emergingMarkets = require('../../assets/countries/emerging-markets.json');
|
||||
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private static readonly MAX_CHART_ITEMS = 250;
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
@ -327,10 +329,10 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
|
||||
lastItem?.netPerformance
|
||||
lastItem?.netPerformance ?? 0
|
||||
);
|
||||
let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
|
||||
lastItem?.netPerformance
|
||||
lastItem?.netPerformance ?? 0
|
||||
);
|
||||
if (isAllTimeHigh && isAllTimeLow) {
|
||||
isAllTimeHigh = false;
|
||||
@ -354,6 +356,78 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public async getChartV2(
|
||||
aImpersonationId: string,
|
||||
aDateRange: DateRange = 'max'
|
||||
): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
userId
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
currency: this.request.user.Settings.currency,
|
||||
currentRateService: this.currentRateService,
|
||||
orders: portfolioOrders
|
||||
});
|
||||
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
if (transactionPoints.length === 0) {
|
||||
return {
|
||||
isAllTimeHigh: false,
|
||||
isAllTimeLow: false,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
const endDate = new Date();
|
||||
|
||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||
|
||||
const daysInMarket = differenceInDays(new Date(), startDate);
|
||||
const step = Math.round(
|
||||
daysInMarket / Math.min(daysInMarket, PortfolioService.MAX_CHART_ITEMS)
|
||||
);
|
||||
|
||||
const items: HistoricalDataItem[] = [];
|
||||
|
||||
let currentEndDate = startDate;
|
||||
|
||||
while (isBefore(currentEndDate, endDate)) {
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate,
|
||||
currentEndDate
|
||||
);
|
||||
|
||||
items.push({
|
||||
date: format(currentEndDate, DATE_FORMAT),
|
||||
value: currentPositions.netPerformancePercentage.toNumber() * 100
|
||||
});
|
||||
|
||||
currentEndDate = addDays(currentEndDate, step);
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
|
||||
if (last(items)?.date !== format(today, DATE_FORMAT)) {
|
||||
// Add today
|
||||
const { netPerformancePercentage } =
|
||||
await portfolioCalculator.getCurrentPositions(startDate, today);
|
||||
items.push({
|
||||
date: format(today, DATE_FORMAT),
|
||||
value: netPerformancePercentage.toNumber() * 100
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isAllTimeHigh: false,
|
||||
isAllTimeLow: false,
|
||||
items: items
|
||||
};
|
||||
}
|
||||
|
||||
public async getDetails(
|
||||
aImpersonationId: string,
|
||||
aUserId: string,
|
||||
@ -466,7 +540,9 @@ export class PortfolioService {
|
||||
|
||||
holdings[item.symbol] = {
|
||||
markets,
|
||||
allocationCurrent: value.div(totalValue).toNumber(),
|
||||
allocationCurrent: totalValue.eq(0)
|
||||
? 0
|
||||
: value.div(totalValue).toNumber(),
|
||||
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
||||
assetClass: symbolProfile.assetClass,
|
||||
assetSubClass: symbolProfile.assetSubClass,
|
||||
@ -478,7 +554,7 @@ export class PortfolioService {
|
||||
item.grossPerformancePercentage?.toNumber() ?? 0,
|
||||
investment: item.investment.toNumber(),
|
||||
marketPrice: item.marketPrice,
|
||||
marketState: dataProviderResponse.marketState,
|
||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||
name: symbolProfile.name,
|
||||
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
||||
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { CacheModule, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { CacheManagerOptions, CacheModule, Module } from '@nestjs/common';
|
||||
import * as redisStore from 'cache-manager-redis-store';
|
||||
|
||||
import { RedisCacheService } from './redis-cache.service';
|
||||
@ -9,16 +8,18 @@ import { RedisCacheService } from './redis-cache.service';
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configurationService: ConfigurationService) => ({
|
||||
imports: [ConfigurationModule],
|
||||
inject: [ConfigurationService],
|
||||
useFactory: async (configurationService: ConfigurationService) => {
|
||||
return <CacheManagerOptions>{
|
||||
host: configurationService.get('REDIS_HOST'),
|
||||
max: configurationService.get('MAX_ITEM_IN_CACHE'),
|
||||
password: configurationService.get('REDIS_PASSWORD'),
|
||||
port: configurationService.get('REDIS_PORT'),
|
||||
store: redisStore,
|
||||
ttl: configurationService.get('CACHE_TTL')
|
||||
})
|
||||
};
|
||||
}
|
||||
}),
|
||||
ConfigurationModule
|
||||
],
|
||||
|
@ -5,6 +5,10 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
emergencyFund?: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isExperimentalFeatures?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isRestrictedView?: boolean;
|
||||
|
@ -43,7 +43,7 @@ export class UserService {
|
||||
include: {
|
||||
User: true
|
||||
},
|
||||
orderBy: { User: { alias: 'asc' } },
|
||||
orderBy: { alias: 'asc' },
|
||||
where: { GranteeUser: { id } }
|
||||
});
|
||||
let tags = await this.tagService.getByUser(id);
|
||||
@ -98,7 +98,6 @@ export class UserService {
|
||||
const {
|
||||
accessToken,
|
||||
Account,
|
||||
alias,
|
||||
authChallenge,
|
||||
createdAt,
|
||||
id,
|
||||
@ -116,7 +115,6 @@ export class UserService {
|
||||
const user: UserWithSettings = {
|
||||
accessToken,
|
||||
Account,
|
||||
alias,
|
||||
authChallenge,
|
||||
createdAt,
|
||||
id,
|
||||
|
@ -17,7 +17,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
|
||||
imports: [
|
||||
BullModule.registerQueue({
|
||||
limiter: {
|
||||
duration: ms('5 seconds'),
|
||||
duration: ms('4 seconds'),
|
||||
max: 1
|
||||
},
|
||||
name: DATA_GATHERING_QUEUE
|
||||
|
@ -6,7 +6,11 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
extractNumberFromString,
|
||||
getYesterday
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
@ -16,8 +20,6 @@ import { addDays, format, isBefore } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
|
||||
public constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
@ -77,7 +79,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
const html = await get();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const value = this.extractNumberFromString($(selector).text());
|
||||
const value = extractNumberFromString($(selector).text());
|
||||
|
||||
return {
|
||||
[symbol]: {
|
||||
@ -175,15 +177,4 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
private extractNumberFromString(aString: string): number {
|
||||
try {
|
||||
const [numberString] = aString.match(
|
||||
GhostfolioScraperApiService.NUMERIC_REGEXP
|
||||
);
|
||||
return parseFloat(numberString.trim());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({
|
||||
exports: [TwitterBotService],
|
||||
imports: [BenchmarkModule, ConfigurationModule, PropertyModule, SymbolModule],
|
||||
imports: [BenchmarkModule, ConfigurationModule, SymbolModule],
|
||||
providers: [TwitterBotService]
|
||||
})
|
||||
export class TwitterBotModule {}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import {
|
||||
PROPERTY_BENCHMARKS,
|
||||
ghostfolioFearAndGreedIndexDataSource,
|
||||
ghostfolioFearAndGreedIndexSymbol
|
||||
} from '@ghostfolio/common/config';
|
||||
@ -11,7 +9,6 @@ import {
|
||||
resolveFearAndGreedIndex,
|
||||
resolveMarketCondition
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { isWeekend } from 'date-fns';
|
||||
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
||||
@ -23,7 +20,6 @@ export class TwitterBotService {
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly symbolService: SymbolService
|
||||
) {
|
||||
this.twitterClient = new TwitterApi({
|
||||
@ -82,14 +78,9 @@ export class TwitterBotService {
|
||||
}
|
||||
|
||||
private async getBenchmarkListing(aMax: number) {
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
|
||||
const benchmarks = await this.benchmarkService.getBenchmarks(
|
||||
benchmarkAssets
|
||||
);
|
||||
const benchmarks = await this.benchmarkService.getBenchmarks({
|
||||
useCache: false
|
||||
});
|
||||
|
||||
const benchmarkListing: string[] = [];
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'client',
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div class="align-items-center d-flex flex-row">
|
||||
<div class="position-relative">
|
||||
<div class="align-items-center d-flex flex-row" [hidden]="!fearAndGreedIndex">
|
||||
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
|
||||
<div>
|
||||
<div class="h4 mb-0">
|
||||
@ -11,3 +12,12 @@
|
||||
<small class="d-block" i18n>Current Market Mood</small>
|
||||
</div>
|
||||
</div>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="!fearAndGreedIndex"
|
||||
animation="pulse"
|
||||
class="position-absolute w-100"
|
||||
[theme]="{
|
||||
height: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
|
@ -1,3 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
ngx-skeleton-loader {
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { FearAndGreedIndexComponent } from './fear-and-greed-index.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FearAndGreedIndexComponent],
|
||||
exports: [FearAndGreedIndexComponent],
|
||||
imports: [CommonModule]
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule]
|
||||
})
|
||||
export class GfFearAndGreedIndexModule {}
|
||||
|
@ -20,7 +20,6 @@
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||
[hidden]="isLoading"
|
||||
></gf-fear-and-greed-index>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,7 +106,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
this.isLoadingPerformance = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({ range: this.dateRange })
|
||||
.fetchChart({
|
||||
range: this.dateRange,
|
||||
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||
|
@ -15,7 +15,6 @@
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[hidden]="historicalDataItems?.length === 0"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -24,6 +23,7 @@
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
[unit]="user?.settings?.isExperimentalFeatures ? '%' : user?.settings?.baseCurrency"
|
||||
></gf-line-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -249,10 +249,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions(
|
||||
this.isInPercent ? undefined : this.currency,
|
||||
this.isInPercent ? undefined : this.locale
|
||||
),
|
||||
...getTooltipOptions({
|
||||
locale: this.isInPercent ? undefined : this.locale,
|
||||
unit: this.isInPercent ? undefined : this.currency
|
||||
}),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
|
@ -23,13 +23,13 @@
|
||||
class="mb-4"
|
||||
benchmarkLabel="Average Unit Price"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[locale]="data.locale"
|
||||
[showGradient]="true"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
></gf-line-chart>
|
||||
|
||||
<div class="row">
|
||||
|
@ -226,6 +226,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onExperimentalFeaturesChange(aEvent: MatSlideToggleChange) {
|
||||
this.dataService
|
||||
.putUserSetting({ isExperimentalFeatures: aEvent.checked })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public onRedeemCoupon() {
|
||||
let couponCode = prompt($localize`Please enter your coupon code:`);
|
||||
couponCode = couponCode?.trim();
|
||||
|
@ -119,6 +119,7 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select
|
||||
name="language"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
[value]="language"
|
||||
(selectionChange)="onChangeUserSetting('language', $event.value)"
|
||||
>
|
||||
@ -187,6 +188,22 @@
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
|
||||
class="align-items-center d-flex mt-4 py-1"
|
||||
>
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Experimental Features</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
[checked]="user.settings.isExperimentalFeatures"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
(change)="onExperimentalFeaturesChange($event)"
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex mt-4 py-1">
|
||||
<div class="pr-1 w-50" i18n>User ID</div>
|
||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||
|
@ -84,7 +84,7 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
href="'../en/blog/2022/01/ghostfolio-first-months-in-open-source"
|
||||
href="../en/blog/2022/01/ghostfolio-first-months-in-open-source"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
|
@ -185,8 +185,8 @@ export class DataService {
|
||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||
}
|
||||
|
||||
public fetchChart({ range }: { range: DateRange }) {
|
||||
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
||||
public fetchChart({ range, version }: { range: DateRange; version: number }) {
|
||||
return this.http.get<PortfolioChart>(`/api/v${version}/portfolio/chart`, {
|
||||
params: { range }
|
||||
});
|
||||
}
|
||||
|
@ -6,70 +6,70 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about/changelog</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/demo</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/features</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/markets</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/pricing</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/register</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources</loc>
|
||||
<lastmod>2022-08-18T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-09-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -642,7 +642,7 @@
|
||||
<target state="translated">Aktuelle Marktstimmung</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
<context context-type="linenumber">12</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||
@ -1058,7 +1058,7 @@
|
||||
<target state="translated">Bitte gib den Betrag deines Notfallfonds ein:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
||||
<context context-type="linenumber">48</context>
|
||||
<context context-type="linenumber">52</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||
@ -1262,7 +1262,7 @@
|
||||
<target state="translated">Bitte gebe deinen Gutscheincode ein:</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">230</context>
|
||||
<context context-type="linenumber">248</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4420880039966769543" datatype="html">
|
||||
@ -1270,7 +1270,7 @@
|
||||
<target state="translated">Gutscheincode konnte nicht eingelöst werden</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">240</context>
|
||||
<context context-type="linenumber">258</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4819099731531004979" datatype="html">
|
||||
@ -1278,7 +1278,7 @@
|
||||
<target state="translated">Gutscheincode wurde eingelöst</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">252</context>
|
||||
<context context-type="linenumber">270</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7967484035994732534" datatype="html">
|
||||
@ -1286,7 +1286,7 @@
|
||||
<target state="translated">Neu laden</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">253</context>
|
||||
<context context-type="linenumber">271</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7963559562180316948" datatype="html">
|
||||
@ -1294,7 +1294,7 @@
|
||||
<target state="translated">Möchtest du diese Anmeldemethode wirklich löschen?</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">299</context>
|
||||
<context context-type="linenumber">317</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||
@ -1382,7 +1382,7 @@
|
||||
<target state="new">Locale</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">134</context>
|
||||
<context context-type="linenumber">135</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
@ -1390,7 +1390,7 @@
|
||||
<target state="translated">Datums- und Zahlenformat</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">136</context>
|
||||
<context context-type="linenumber">137</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
@ -1398,7 +1398,7 @@
|
||||
<target state="translated">Ansicht</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">159</context>
|
||||
<context context-type="linenumber">160</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
@ -1406,7 +1406,7 @@
|
||||
<target state="translated">Einloggen mit Fingerabdruck</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">180</context>
|
||||
<context context-type="linenumber">181</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
@ -1414,7 +1414,7 @@
|
||||
<target state="translated">Benutzer ID</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">191</context>
|
||||
<context context-type="linenumber">208</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
@ -1422,7 +1422,7 @@
|
||||
<target state="translated">Zugangsberechtigung</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">200</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -2641,6 +2641,14 @@
|
||||
<context context-type="linenumber">4,7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
|
||||
<source>Experimental Features</source>
|
||||
<target state="translated">Experimentelle Funktionen</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">196</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
@ -583,7 +583,7 @@
|
||||
<source>Current Market Mood</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
<context context-type="linenumber">12</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
|
||||
@ -957,7 +957,7 @@
|
||||
<source>Please enter the amount of your emergency fund:</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts</context>
|
||||
<context context-type="linenumber">48</context>
|
||||
<context context-type="linenumber">52</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="fc61416d48adb7af122b8697e806077eb251fb57" datatype="html">
|
||||
@ -1137,35 +1137,35 @@
|
||||
<source>Please enter your coupon code:</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">230</context>
|
||||
<context context-type="linenumber">248</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4420880039966769543" datatype="html">
|
||||
<source>Could not redeem coupon code</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">240</context>
|
||||
<context context-type="linenumber">258</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4819099731531004979" datatype="html">
|
||||
<source>Coupon code has been redeemed</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">252</context>
|
||||
<context context-type="linenumber">270</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7967484035994732534" datatype="html">
|
||||
<source>Reload</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">253</context>
|
||||
<context context-type="linenumber">271</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7963559562180316948" datatype="html">
|
||||
<source>Do you really want to remove this sign in method?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.component.ts</context>
|
||||
<context context-type="linenumber">299</context>
|
||||
<context context-type="linenumber">317</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="29881a45dafbe5aa05cd9d0441a4c0c2fb06df92" datatype="html">
|
||||
@ -1243,42 +1243,42 @@
|
||||
<source>Locale</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">134</context>
|
||||
<context context-type="linenumber">135</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4402006eb2c97591dd8c87a5bd8f721fe9e4dc00" datatype="html">
|
||||
<source>Date and number format</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">136</context>
|
||||
<context context-type="linenumber">137</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="234d001ccf20d47ac6a2846bb029eebb61444d15" datatype="html">
|
||||
<source>View Mode</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">159</context>
|
||||
<context context-type="linenumber">160</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ae348ee3a7319c2fc4794fa8bc425999d355f8f" datatype="html">
|
||||
<source>Sign in with fingerprint</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">180</context>
|
||||
<context context-type="linenumber">181</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="83c4d4d764d2e2725ab8e919ec16ac400e1f290a" datatype="html">
|
||||
<source>User ID</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">191</context>
|
||||
<context context-type="linenumber">208</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021c579c084e68d9db06a569d76f024111c6c54" datatype="html">
|
||||
<source>Granted Access</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">200</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5e41f1b4c46ad9e0a9bc83fa36445483aa5cc324" datatype="html">
|
||||
@ -2359,6 +2359,13 @@
|
||||
<context context-type="linenumber">6</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="03b120b05e0922e5e830c3466fda9ee0bfbf59e9" datatype="html">
|
||||
<source>Experimental Features</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/account/account-page.html</context>
|
||||
<context context-type="linenumber">196</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'common',
|
||||
|
||||
|
@ -2,7 +2,13 @@ import { Chart, TooltipPosition } from 'chart.js';
|
||||
|
||||
import { getBackgroundColor, getTextColor } from './helper';
|
||||
|
||||
export function getTooltipOptions(currency = '', locale = '') {
|
||||
export function getTooltipOptions({
|
||||
locale = '',
|
||||
unit = ''
|
||||
}: {
|
||||
locale?: string;
|
||||
unit?: string;
|
||||
} = {}) {
|
||||
return {
|
||||
backgroundColor: getBackgroundColor(),
|
||||
bodyColor: `rgb(${getTextColor()})`,
|
||||
@ -15,11 +21,11 @@ export function getTooltipOptions(currency = '', locale = '') {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
if (currency) {
|
||||
if (unit) {
|
||||
label += `${context.parsed.y.toLocaleString(locale, {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2
|
||||
})} ${currency}`;
|
||||
})} ${unit}`;
|
||||
} else {
|
||||
label += context.parsed.y.toFixed(2);
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import { de } from 'date-fns/locale';
|
||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||
import { Benchmark } from './interfaces';
|
||||
|
||||
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
|
||||
export function capitalize(aString: string) {
|
||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||
}
|
||||
@ -43,6 +45,15 @@ export function encodeDataSource(aDataSource: DataSource) {
|
||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||
}
|
||||
|
||||
export function extractNumberFromString(aString: string): number {
|
||||
try {
|
||||
const [numberString] = aString.match(NUMERIC_REGEXP);
|
||||
return parseFloat(numberString.trim());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBackgroundColor() {
|
||||
return getCssVariable(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
@ -2,6 +2,7 @@ import { ViewMode } from '@prisma/client';
|
||||
|
||||
export interface UserSettings {
|
||||
baseCurrency?: string;
|
||||
isExperimentalFeatures?: boolean;
|
||||
isRestrictedView?: boolean;
|
||||
language?: string;
|
||||
locale: string;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'ui',
|
||||
|
||||
|
@ -47,7 +47,6 @@ import { LineChartItem } from './interfaces/line-chart.interface';
|
||||
export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
@Input() benchmarkDataItems: LineChartItem[] = [];
|
||||
@Input() benchmarkLabel = '';
|
||||
@Input() currency: string;
|
||||
@Input() historicalDataItems: LineChartItem[];
|
||||
@Input() locale: string;
|
||||
@Input() showGradient = false;
|
||||
@ -56,6 +55,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
@Input() showXAxis = false;
|
||||
@Input() showYAxis = false;
|
||||
@Input() symbol: string;
|
||||
@Input() unit: string;
|
||||
@Input() yMax: number;
|
||||
@Input() yMaxLabel: string;
|
||||
@Input() yMin: number;
|
||||
@ -259,7 +259,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions(this.currency, this.locale),
|
||||
...getTooltipOptions({ locale: this.locale, unit: this.unit }),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
|
@ -349,7 +349,7 @@ export class PortfolioProportionChartComponent
|
||||
|
||||
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) {
|
||||
return {
|
||||
...getTooltipOptions(this.baseCurrency, this.locale),
|
||||
...getTooltipOptions({ locale: this.locale, unit: this.baseCurrency }),
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const labelIndex =
|
||||
|
77
package.json
77
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.184.1",
|
||||
"version": "1.187.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -53,16 +53,16 @@
|
||||
"workspace-generator": "nx workspace-generator"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "14.1.0",
|
||||
"@angular/cdk": "14.1.0",
|
||||
"@angular/common": "14.1.0",
|
||||
"@angular/compiler": "14.1.0",
|
||||
"@angular/core": "14.1.0",
|
||||
"@angular/forms": "14.1.0",
|
||||
"@angular/material": "14.1.0",
|
||||
"@angular/platform-browser": "14.1.0",
|
||||
"@angular/platform-browser-dynamic": "14.1.0",
|
||||
"@angular/router": "14.1.0",
|
||||
"@angular/animations": "14.2.0",
|
||||
"@angular/cdk": "14.2.0",
|
||||
"@angular/common": "14.2.0",
|
||||
"@angular/compiler": "14.2.0",
|
||||
"@angular/core": "14.2.0",
|
||||
"@angular/forms": "14.2.0",
|
||||
"@angular/material": "14.2.0",
|
||||
"@angular/platform-browser": "14.2.0",
|
||||
"@angular/platform-browser-dynamic": "14.2.0",
|
||||
"@angular/router": "14.2.0",
|
||||
"@codewithdan/observable-store": "2.2.11",
|
||||
"@dfinity/agent": "0.12.1",
|
||||
"@dfinity/auth-client": "0.12.1",
|
||||
@ -80,7 +80,7 @@
|
||||
"@nestjs/platform-express": "9.0.7",
|
||||
"@nestjs/schedule": "2.1.0",
|
||||
"@nestjs/serve-static": "3.0.0",
|
||||
"@nrwl/angular": "14.5.1",
|
||||
"@nrwl/angular": "14.6.4",
|
||||
"@prisma/client": "4.1.1",
|
||||
"@simplewebauthn/browser": "5.2.1",
|
||||
"@simplewebauthn/server": "5.2.1",
|
||||
@ -121,34 +121,34 @@
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "4.1.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.4.0",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "8.199.0",
|
||||
"svgmap": "2.6.0",
|
||||
"twitter-api-v2": "1.10.3",
|
||||
"uuid": "8.3.2",
|
||||
"yahoo-finance2": "2.3.3",
|
||||
"zone.js": "0.11.6"
|
||||
"zone.js": "0.11.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "14.1.0",
|
||||
"@angular-eslint/eslint-plugin": "14.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "14.0.2",
|
||||
"@angular-eslint/template-parser": "14.0.2",
|
||||
"@angular/cli": "14.1.0",
|
||||
"@angular/compiler-cli": "14.1.0",
|
||||
"@angular/language-service": "14.1.0",
|
||||
"@angular/localize": "14.1.0",
|
||||
"@angular-devkit/build-angular": "14.2.1",
|
||||
"@angular-eslint/eslint-plugin": "14.0.3",
|
||||
"@angular-eslint/eslint-plugin-template": "14.0.3",
|
||||
"@angular-eslint/template-parser": "14.0.3",
|
||||
"@angular/cli": "14.2.1",
|
||||
"@angular/compiler-cli": "14.2.0",
|
||||
"@angular/language-service": "14.2.0",
|
||||
"@angular/localize": "14.2.0",
|
||||
"@nestjs/schematics": "9.0.1",
|
||||
"@nestjs/testing": "9.0.7",
|
||||
"@nrwl/cli": "14.5.1",
|
||||
"@nrwl/cypress": "14.5.1",
|
||||
"@nrwl/eslint-plugin-nx": "14.5.1",
|
||||
"@nrwl/jest": "14.5.1",
|
||||
"@nrwl/nest": "14.5.1",
|
||||
"@nrwl/node": "14.5.1",
|
||||
"@nrwl/nx-cloud": "14.2.0",
|
||||
"@nrwl/storybook": "14.5.1",
|
||||
"@nrwl/workspace": "14.5.1",
|
||||
"@nrwl/cli": "14.6.4",
|
||||
"@nrwl/cypress": "14.6.4",
|
||||
"@nrwl/eslint-plugin-nx": "14.6.4",
|
||||
"@nrwl/jest": "14.6.4",
|
||||
"@nrwl/nest": "14.6.4",
|
||||
"@nrwl/node": "14.6.4",
|
||||
"@nrwl/nx-cloud": "14.6.1",
|
||||
"@nrwl/storybook": "14.6.4",
|
||||
"@nrwl/workspace": "14.6.4",
|
||||
"@simplewebauthn/typescript-types": "5.2.1",
|
||||
"@storybook/addon-essentials": "6.5.9",
|
||||
"@storybook/angular": "6.5.9",
|
||||
@ -160,9 +160,9 @@
|
||||
"@types/cache-manager": "3.4.2",
|
||||
"@types/color": "3.0.2",
|
||||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/jest": "28.1.8",
|
||||
"@types/lodash": "4.14.174",
|
||||
"@types/node": "16.11.7",
|
||||
"@types/node": "18.7.1",
|
||||
"@types/papaparse": "5.2.6",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.4.0",
|
||||
@ -176,14 +176,15 @@
|
||||
"import-sort-cli": "6.0.0",
|
||||
"import-sort-parser-typescript": "6.0.0",
|
||||
"import-sort-style-module": "6.0.0",
|
||||
"jest": "27.5.1",
|
||||
"jest-preset-angular": "11.1.2",
|
||||
"nx": "14.5.1",
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "28.1.1",
|
||||
"jest-preset-angular": "12.2.2",
|
||||
"nx": "14.6.4",
|
||||
"prettier": "2.7.1",
|
||||
"replace-in-file": "6.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"ts-node": "10.8.1",
|
||||
"ts-jest": "28.0.8",
|
||||
"ts-node": "10.9.1",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "alias";
|
@ -160,7 +160,6 @@ model Tag {
|
||||
|
||||
model User {
|
||||
accessToken String?
|
||||
alias String?
|
||||
authChallenge String?
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(uuid())
|
||||
|
@ -111,7 +111,6 @@ async function main() {
|
||||
}
|
||||
]
|
||||
},
|
||||
alias: 'Demo',
|
||||
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
|
||||
role: Role.DEMO
|
||||
},
|
||||
|
@ -1,5 +0,0 @@
|
||||
set -xe
|
||||
echo "$DOCKER_HUB_ACCESS_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
|
||||
|
||||
docker build -t ghostfolio/ghostfolio:$TRAVIS_TAG -t ghostfolio/ghostfolio:latest .
|
||||
docker push ghostfolio/ghostfolio --all-tags
|
Reference in New Issue
Block a user