feat(web): enable runtime PWA configuration

This commit is contained in:
4gray committed 2026-05-15 11:20:35 +02:00
1 parent f0af34edef
commit eafc95bef0
9 files changed
+110 -37

No files matched your search

+2 -1
View File
@@ -40,6 +40,7 @@ import {
} from './services/portal-navigation-actions.service';
import { providePortalPlaybackPositions } from './services/portal-playback-positions.service';
import { PwaService } from './services/pwa.service';
import { shouldEnableServiceWorker } from './services/runtime-config';
import { provideWorkspaceShellActions } from './services/workspace-shell-actions.service';
// AoT requires an exported function for factories
@@ -113,7 +114,7 @@ export const appConfig: ApplicationConfig = {
provideRouterStore(),
...(AppConfig.production ? [] : [provideStoreDevtools({ maxAge: 25 })]),
provideServiceWorker('ngsw-worker.js', {
enabled: AppConfig.production && !!window.electron,
enabled: shouldEnableServiceWorker(),
registrationStrategy: 'registerWhenStable:30000',
}),
importProvidersFrom(
+2 -1
View File
@@ -25,6 +25,7 @@ import {
logPortalDebugEvent,
logPortalDebugRequest,
} from '@iptvnator/portal/shared/util';
import { getRuntimeBackendUrl } from './runtime-config';
interface PwaXtreamResponse {
readonly payload?: unknown;
@@ -69,7 +70,7 @@ export class PwaService extends DataService {
]);
/** Proxy URL to avoid CORS issues */
corsProxyUrl = AppConfig.BACKEND_URL;
corsProxyUrl = getRuntimeBackendUrl();
constructor() {
super();
@@ -0,0 +1,39 @@
import { resolveBackendUrl, shouldEnableServiceWorker } from './runtime-config';
describe('runtime config helpers', () => {
it('uses runtime BACKEND_URL when provided', () => {
expect(
resolveBackendUrl(
{ BACKEND_URL: ' http://self-hosted.local/api ' },
'https://fallback.example'
)
).toBe('http://self-hosted.local/api');
});
it('falls back to the build-time backend URL when runtime config is missing or blank', () => {
expect(resolveBackendUrl(undefined, 'https://fallback.example')).toBe(
'https://fallback.example'
);
expect(resolveBackendUrl({}, 'https://fallback.example')).toBe(
'https://fallback.example'
);
expect(
resolveBackendUrl(
{ BACKEND_URL: ' ' },
'https://fallback.example'
)
).toBe('https://fallback.example');
});
it('enables service worker only for production builds with browser support', () => {
expect(
shouldEnableServiceWorker(true, {
serviceWorker: {},
} as Navigator)
).toBe(true);
expect(
shouldEnableServiceWorker(false, { serviceWorker: {} } as Navigator)
).toBe(false);
expect(shouldEnableServiceWorker(true, {} as Navigator)).toBe(false);
});
});
@@ -0,0 +1,27 @@
import { AppConfig } from '../../environments/environment';
export interface IptvnatorRuntimeConfig {
readonly BACKEND_URL?: string;
}
export function resolveBackendUrl(
runtimeConfig: IptvnatorRuntimeConfig | undefined,
fallbackUrl: string
): string {
const runtimeUrl = runtimeConfig?.BACKEND_URL?.trim();
return runtimeUrl || fallbackUrl;
}
export function getRuntimeBackendUrl(): string {
return resolveBackendUrl(
globalThis.window?.__IPTVNATOR_CONFIG__,
AppConfig.BACKEND_URL
);
}
export function shouldEnableServiceWorker(
production = AppConfig.production,
navigatorRef: Navigator | undefined = globalThis.navigator
): boolean {
return production && !!navigatorRef && 'serviceWorker' in navigatorRef;
}
+1
View File
@@ -0,0 +1 @@
window.__IPTVNATOR_CONFIG__ = window.__IPTVNATOR_CONFIG__ || {};
+2 -5
View File
@@ -20,6 +20,7 @@
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico" />
<script src="assets/app-config.js"></script>
<style>
/* Inline splash — paints immediately while the JS bundle and the
styles.css (~300KB) are still downloading and Angular is
@@ -68,11 +69,7 @@
</style>
</head>
<body class="mat-app-background">
<div
id="initial-splash"
role="status"
aria-label="Loading IPTVnator"
>
<div id="initial-splash" role="status" aria-label="Loading IPTVnator">
<span class="splash-mark">IPTVnator</span>
<span class="splash-spinner" aria-hidden="true"></span>
</div>