Browse Source

Merge branch 'main' into task/remove-deprecated-symbol-profile-from-activity-interface

pull/7360/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
8c0c4cbc0a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 26
      apps/api/src/app/app.module.ts
  3. 3
      apps/api/src/main.ts
  4. 8
      apps/api/src/middlewares/html-template.middleware.ts
  5. 37
      apps/api/src/middlewares/language-redirect.middleware.ts
  6. 3
      apps/client/project.json
  7. 27
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  8. 0
      apps/client/src/assets/index.html
  9. 2
      apps/client/src/index.html

6
CHANGELOG.md

@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Removed the deprecated `SymbolProfile` field from the activity interface - Removed the deprecated `SymbolProfile` field from the activity interface
- Refactored the language redirect of the root path from the static file serving configuration to a dedicated middleware
### Fixed
- Fixed the `RangeNotSatisfiableError` for requests with a `Range` header to the root path caused by the empty `index.html` placeholder
- Fixed the unresolved template literal in the page title while the app is loading from the service worker cache
## 3.30.0 - 2026-07-19 ## 3.30.0 - 2026-07-19

26
apps/api/src/app/app.module.ts

@ -14,8 +14,6 @@ import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-g
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { import {
BULL_BOARD_ROUTE, BULL_BOARD_ROUTE,
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES,
THROTTLE_DEFAULT_LIMIT, THROTTLE_DEFAULT_LIMIT,
THROTTLE_DEFAULT_TTL THROTTLE_DEFAULT_TTL
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -143,29 +141,7 @@ import { UserModule } from './user/user.module';
'/api/*wildcard', '/api/*wildcard',
'/sitemap.xml' '/sitemap.xml'
], ],
rootPath: join(__dirname, '..', 'client'), rootPath: join(__dirname, '..', 'client')
serveStaticOptions: {
setHeaders: (res) => {
if (res.req?.path === '/') {
let languageCode = DEFAULT_LANGUAGE_CODE;
try {
const code = res.req.headers['accept-language']
.split(',')[0]
.split('-')[0];
if (
(SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)
) {
languageCode = code;
}
} catch {}
res.set('Location', `/${languageCode}`);
res.statusCode = StatusCodes.MOVED_PERMANENTLY;
}
}
}
}), }),
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', '.well-known'), rootPath: join(__dirname, '..', 'client', '.well-known'),

3
apps/api/src/main.ts

@ -1,3 +1,4 @@
import { languageRedirectMiddleware } from '@ghostfolio/api/middlewares/language-redirect.middleware';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { import {
BULL_BOARD_ROUTE, BULL_BOARD_ROUTE,
@ -100,6 +101,8 @@ async function bootstrap() {
}); });
} }
app.use(languageRedirectMiddleware);
const configurationService = app.get(ConfigurationService); const configurationService = app.get(ConfigurationService);
const trustProxy = configurationService.get('TRUST_PROXY'); const trustProxy = configurationService.get('TRUST_PROXY');

8
apps/api/src/middlewares/html-template.middleware.ts

@ -111,7 +111,13 @@ export class HtmlTemplateMiddleware implements NestMiddleware {
); );
try { try {
map[languageCode] = readFileSync(indexHtmlPath, 'utf8'); // Restore the interpolation token which the template replaces with a
// static fallback title to avoid showing an unresolved template
// literal when served without interpolation (e.g. by the service worker)
map[languageCode] = readFileSync(indexHtmlPath, 'utf8').replace(
/<title>.*?<\/title>/,
'<title>${title}</title>'
);
} catch { } catch {
this.logger.warn( this.logger.warn(
`Skipping language '${languageCode}': ${indexHtmlPath} not found` `Skipping language '${languageCode}': ${indexHtmlPath} not found`

37
apps/api/src/middlewares/language-redirect.middleware.ts

@ -0,0 +1,37 @@
import { environment } from '@ghostfolio/api/environments/environment';
import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { NextFunction, Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
export function languageRedirectMiddleware(
request: Request,
response: Response,
next: NextFunction
) {
if (
!environment.production ||
request.path !== '/' ||
!['GET', 'HEAD'].includes(request.method)
) {
return next();
}
let languageCode = DEFAULT_LANGUAGE_CODE;
try {
const code = request.headers['accept-language'].split(',')[0].split('-')[0];
if ((SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)) {
languageCode = code;
}
} catch {}
return response.redirect(
StatusCodes.MOVED_PERMANENTLY,
`/${languageCode}${request.url.slice(1)}`
);
}

3
apps/client/project.json

@ -203,9 +203,6 @@
{ {
"command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client" "command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client"
}, },
{
"command": "shx cp apps/client/src/assets/index.html dist/apps/client"
},
{ {
"command": "shx cp apps/client/src/assets/robots.txt dist/apps/client" "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client"
}, },

27
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -213,9 +213,16 @@ export class GfAdminOverviewComponent implements OnInit {
duration: this.couponDuration duration: this.couponDuration
}; };
const hasCopiedCouponCode = this.clipboard.copy(newCoupon.code);
const coupons = [...this.couponsDataSource.data, newCoupon]; const coupons = [...this.couponsDataSource.data, newCoupon];
this.saveCoupons({ coupons, codeToCopy: newCoupon.code }); this.saveCoupons({
coupons,
snackBarMessage: hasCopiedCouponCode
? '✅ ' + $localize`${newCoupon.code} has been copied to the clipboard`
: '✅ ' + $localize`Coupon ${newCoupon.code} has been created`
});
} }
protected onChangeCouponDuration(aCouponDuration: StringValue) { protected onChangeCouponDuration(aCouponDuration: StringValue) {
@ -374,11 +381,11 @@ export class GfAdminOverviewComponent implements OnInit {
} }
private saveCoupons({ private saveCoupons({
codeToCopy, coupons,
coupons snackBarMessage
}: { }: {
codeToCopy?: string;
coupons: Coupon[]; coupons: Coupon[];
snackBarMessage?: string;
}) { }) {
this.dataService this.dataService
.putAdminSetting(PROPERTY_COUPONS, { .putAdminSetting(PROPERTY_COUPONS, {
@ -388,14 +395,10 @@ export class GfAdminOverviewComponent implements OnInit {
.subscribe(() => { .subscribe(() => {
this.couponsDataSource.data = coupons; this.couponsDataSource.data = coupons;
if (codeToCopy) { if (snackBarMessage) {
this.clipboard.copy(codeToCopy); this.snackBar.open(snackBarMessage, undefined, {
duration: ms('3 seconds')
this.snackBar.open( });
'✅ ' + $localize`${codeToCopy} has been copied to the clipboard`,
undefined,
{ duration: ms('3 seconds') }
);
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

0
apps/client/src/assets/index.html

2
apps/client/src/index.html

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html class="h-100 position-relative" lang="${languageCode}"> <html class="h-100 position-relative" lang="${languageCode}">
<head> <head>
<title>${title}</title> <title>Ghostfolio</title>
<base href="/" /> <base href="/" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="yes" name="apple-mobile-web-app-capable" />

Loading…
Cancel
Save