mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
* feat(interfaces): add the server storage HTTP contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): read server storage configuration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open the server database and track revisions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): playlist and user-state repositories Playlist documents use the shared desktop row codec, so the server writes the same rows. keepPayload upserts take the stored playlist/items/header fields when the client leaves them out. Xtream user data lives in the server-only server_user_state table, one JSON value per namespace and playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): storage error mapping and access token guard Storage errors map to JSON status bodies and are logged by name and code only. An optional bearer token guards every route except /, /health, /config.js and /v1/instance, provider proxy routes included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): serve the /v1 server storage API /v1/instance is always public and reports whether server storage is on. With storage, /v1/playlists serves whole documents with ETags and 304s, accepts keepPayload upserts, and deletes playlists with their user state; /v1/user-state and /v1/app-state store the rest. The proxy routes are unchanged unless an access token is configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open server storage on start and close on shutdown With IPTVNATOR_STORAGE=server, main.ts opens the database before it listens and exits with a clear message when the data directory is not writable. SIGTERM and SIGINT close the server and the database. The native better-sqlite3 module stays outside the esbuild bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): match weakened ETags on playlist reads nginx weakens the ETag of a gzipped response, and clients send the weak form back in If-None-Match. The route now compares tags weakly, as RFC 9110 requires for GET, so it answers 304 before reading the document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(docker): persist server storage on a volume The image builds better-sqlite3 in a native stage per target platform, enables server storage on the /data volume, and raises the nginx body limit to 64 MB for /api with gzip for JSON. The compose file mounts a named volume and documents the optional access token. The PWA does not use the storage yet; docs say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web-backend): type the storage route responses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): harden server storage reads, deletes and app state - Metadata reads return the stored document without its payload fields, so fields that live only in the JSON survive a metadata round-trip. - A conditional GET checks that the playlist exists before answering 304. - Deleting a playlist and its user state commits in one transaction; the ETag moves only after the commit. - Browser app state is stored under a client: prefix and the instance id is repaired when empty, so a client write cannot stop the backend. - User-state buckets have no prototype, so ids like __proto__ round-trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(self-hosted): keep the access token unset until the PWA sends it Add the release note for the Docker server storage volume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): bound shutdown and reject padded playlist ids - SIGTERM closes idle connections at once and cuts the rest after five seconds, so a long provider download cannot keep the backend running. - Playlist ids with surrounding whitespace are rejected: the row codec trims ids, so storage and ETags would track different keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): clear optional columns on whole-document upserts The row codec maps empty values to undefined, which ON CONFLICT DO UPDATE skips, so a cleared User-Agent, Referrer or credential kept its old column value and came back on the next read. Server upserts now write NULL to every replaceable column the document no longer sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): linear token parsing, no-store app state, longer start - The access guard reads the Bearer token by string scanning instead of a regular expression that CodeQL flagged as polynomial on long whitespace. - GET /v1/app-state/:key sends Cache-Control: no-store like the other state routes, so devices never reuse a stale value. - The entrypoint waits up to IPTVNATOR_BACKEND_START_TIMEOUT (300 s) for /health, because the backend migrates its database before it listens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): metadata reads skip payloads, redact JSON parse warnings List playlist metadata from a server-only payload-free copy of each document instead of running json_remove over every payload blob, and log only the error name when a stored JSON value fails to parse, since the parser's message quotes the stored text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): copy carried-over panel clock into playlist metadata When an upsert keeps the stored serverTimezone, build the metadata copy from the saved payload so metadata and full reads agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
371 lines
19 KiB
Markdown
371 lines
19 KiB
Markdown
# PWA Self-hosted Architecture
|
|
|
|
This document describes the browser PWA and self-hosted Docker path.
|
|
|
|
## Ownership
|
|
|
|
- `apps/web` owns the Angular browser UI and PWA service worker configuration.
|
|
- `apps/web-backend` owns the browser-only backend proxy for remote playlist,
|
|
Xtream, and Stalker requests.
|
|
- `docker/` owns the production self-hosted image that bundles the PWA and
|
|
`web-backend` into one container.
|
|
|
|
The old external `4gray/iptvnator-backend` repository is not required for the
|
|
default self-hosted deployment. Sync behavior from that repository only when a
|
|
change intentionally restores or imports missing backend capabilities.
|
|
|
|
## Runtime Backend Configuration
|
|
|
|
The PWA reads `window.__IPTVNATOR_CONFIG__.BACKEND_URL` through
|
|
`apps/web/src/app/services/runtime-config.ts`. The static placeholder lives at
|
|
`apps/web/src/assets/app-config.js` and keeps hosted builds working without
|
|
Docker-specific values.
|
|
|
|
The Docker entrypoint rewrites `assets/app-config.js` at container startup.
|
|
`ngsw-config.json` explicitly excludes this file from Angular service worker
|
|
asset hashing so a runtime rewrite does not break cache validation.
|
|
|
|
## Service Worker Build
|
|
|
|
Use the PWA build configuration for browser deployments:
|
|
|
|
```bash
|
|
pnpm nx build web --configuration=pwa
|
|
```
|
|
|
|
The build must emit these files in `dist/apps/web`:
|
|
|
|
- `ngsw-worker.js`
|
|
- `ngsw.json`
|
|
- `safety-worker.js`
|
|
- `worker-basic.min.js`
|
|
|
|
Angular also emits hashed font and media assets under `dist/apps/web/media/`.
|
|
Keep `/media/**` in `ngsw-config.json` so the PWA service worker can cache
|
|
bundled fonts, including Material Icons.
|
|
|
|
The Angular service worker is a browser/PWA feature only. Packaged Electron
|
|
loads the same Angular production bundle from `file://.../app.asar/web`, but it
|
|
must not register `ngsw-worker.js`; otherwise a desktop update can leave the
|
|
first Electron window controlled by a stale file-origin service worker and serve
|
|
old chunks from Electron `userData`. Electron clears legacy `serviceworkers` and
|
|
`cachestorage` storage from its default session before loading the packaged
|
|
renderer so existing desktop installs recover on the next startup without
|
|
clearing unrelated app storage.
|
|
|
|
`web:serve-static` serves `dist/apps/web` and builds with `web:build:pwa`, so it
|
|
exercises the same output layout as Docker. If Nx daemon state returns stale
|
|
service worker outputs while changing build options, run:
|
|
|
|
```bash
|
|
pnpm nx reset
|
|
pnpm nx build web --configuration=pwa --skip-nx-cache
|
|
```
|
|
|
|
## Web Backend
|
|
|
|
The current self-hosted PWA uses these `apps/web-backend` routes:
|
|
|
|
- `GET /health`
|
|
- `GET /config.js`
|
|
- `POST /provider-targets` with `{ "url": "<provider-url>" }`
|
|
- `GET /parse?targetId=<id>`
|
|
- `GET /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
|
|
- `GET /stalker?targetId=<id>&macAddress=<mac>&action=<action>`
|
|
|
|
Server storage adds versioned routes under `/v1`; the PWA does not call them
|
|
yet:
|
|
|
|
- `GET /v1/instance` (always public): `{ service, apiVersion, instanceId,
|
|
storage: 'server' | 'none', auth: 'none' | 'token' }`
|
|
- `GET /v1/instance/status`: playlist count, database size and path
|
|
- `GET /v1/playlists[?view=meta]`, `GET|PUT|DELETE /v1/playlists/:id`,
|
|
`DELETE /v1/playlists`: whole playlist documents; `GET` sends an `ETag`
|
|
and answers `304` to a matching `If-None-Match`, and
|
|
`PUT ...?keepPayload=1` keeps the stored `playlist`, `items` and
|
|
`header` fields (`409 payload_missing` without a stored row)
|
|
- `GET /v1/user-state`, `PUT|DELETE /v1/user-state/:namespace/:playlistId`:
|
|
Xtream favorites, recently viewed, collection snapshots and playback
|
|
positions, one JSON value per namespace and playlist
|
|
- `GET|PUT /v1/app-state/:key`
|
|
|
|
Server storage lives in `apps/web-backend/src/app/storage/`. With
|
|
`IPTVNATOR_STORAGE=server`, `main.ts` opens the shared database through
|
|
`initDatabase()` (path from `IPTVNATOR_DATA_DIR`), so the server has the same
|
|
schema and migrations as the desktop app and writes playlists with the
|
|
shared row codec. It adds two server-only tables: `server_user_state` for
|
|
Xtream user data, and `server_playlist_meta`, a copy of each document
|
|
without its M3U payload fields that metadata reads use instead of walking
|
|
the payload blobs (filled once on open for rows written without it). ETags
|
|
come from an in-memory revision counter; the server is the only writer of
|
|
its database. Storage errors are logged by error name and code only, because
|
|
Drizzle messages can carry bound values such as portal credentials. With
|
|
`IPTVNATOR_ACCESS_TOKEN`, every route except `/`, `/health`, `/config.js`
|
|
and `/v1/instance` requires `Authorization: Bearer <token>`, the proxy
|
|
routes included. The PWA cannot send that token yet, so the variable must
|
|
stay unset until the browser side lands. Browser app state written through
|
|
`/v1/app-state` is stored under a `client:` key prefix, apart from
|
|
server-owned keys such as the instance id.
|
|
|
|
`pnpm nx run web-backend:serve` starts the backend by hand on port 3333 with
|
|
`TSX_TSCONFIG_PATH=tsconfig.base.json`, which `tsx` needs for the
|
|
`@iptvnator/*` path aliases. `apps/web-e2e/playwright.config.ts` does not use
|
|
that target: it launches `node --import tsx apps/web-backend/src/main.ts` with
|
|
an `env` that mirrors the target's. Nx `run-commands` starts its command in a
|
|
detached process group, and Playwright stops a `webServer` with a
|
|
process-group `SIGKILL` (`taskkill /T /F` on Windows). The kill therefore
|
|
reached only `pnpm`/`nx`, and the orphaned server kept port 3333 after the run.
|
|
`apps/web-backend/src/app/project-config.spec.ts` fails if a Playwright config
|
|
launches the backend through Nx or its `env` drifts from the `serve` target.
|
|
`WEB_BACKEND_PORT` (default 3333) relocates the E2E backend and the
|
|
`self-hosted.e2e.ts` URLs, like `MOCK_PORT` does for the mocks. Use it when
|
|
another worktree holds 3333. The `web:serve` entry can stay on Nx:
|
|
`@angular/build:dev-server` runs inside the Nx process, so the group kill
|
|
stops it. Playwright launches that command itself even outside CI: the
|
|
web-e2e targets carry no Nx `serve` dependency, because Nx refuses a
|
|
non-parallel task with a continuous dependency. The mock servers follow the
|
|
same rule; see [Xtream mock Playwright integration](xtream-mock-server.md#playwright-integration).
|
|
|
|
The PWA continues to use `PwaService`; only the backend base URL is resolved at
|
|
runtime. Electron routes remain owned by the Electron backend and preload
|
|
bridge.
|
|
|
|
Renderer code that needs to branch by runtime should use
|
|
`RuntimeCapabilitiesService` from `@iptvnator/services` instead of adding new
|
|
direct `window.electron` or `DataService.getAppEnvironment()` checks. Keep
|
|
feature decisions expressed as capabilities such as `supportsEpg`,
|
|
`supportsSqlite`, `supportsXtreamSqliteDataSource`, `supportsDownloads`, or
|
|
`supportsManagedExternalPlayers` so PWA and Electron behavior stays auditable
|
|
from one shared boundary. `supportsSqlite` requires the complete playlist
|
|
storage preload API surface used by `PlaylistsService`, `supportsDownloads`
|
|
requires the complete downloads preload API surface used by `DownloadsService`,
|
|
and EPG renderer code should use `EpgRuntimeBridgeService` from
|
|
`@iptvnator/epg/data-access` instead of calling `window.electron` directly.
|
|
Playback-position renderer code should use `PlaybackPositionRuntimeBridgeService`
|
|
from `@iptvnator/services` for Electron SQLite persistence and external-player
|
|
position update events; `supportsPlaybackPositionStorage` and
|
|
`supportsPlaybackPositionUpdates` describe those surfaces independently so PWA
|
|
and partial Electron bridges can degrade without direct preload checks.
|
|
`supportsEpg` remains the aggregate full EPG capability, while narrower runtime
|
|
capabilities cover individual EPG surfaces: import/progress, current-program
|
|
lookup, optional current-program batch reads, optional channel metadata,
|
|
freshness checks, clear/force-fetch management, channel browsing, and program
|
|
search. `supportsPlaylistRefresh` requires the native playlist
|
|
refresh/cancel/progress bridge, `supportsXtreamSectionNavigation` is available
|
|
in PWA and in Electron when either the SQLite Xtream data source or the Xtream
|
|
API transport is available, `supportsDesktopFileSave` requires both
|
|
`saveFileDialog` and `writeFile`, `supportsManagedExternalPlayers` requires the
|
|
MPV and VLC preload launch methods (`openInMpv` and `openInVlc`), and
|
|
`supportsExternalPlayerPathSettings` requires the path-setting methods
|
|
(`setMpvPlayerPath` and `setVlcPlayerPath`); a partial Electron bridge must not
|
|
expose desktop-only actions in the PWA/shared UI.
|
|
|
|
## Runtime Limitations
|
|
|
|
The self-hosted build is the browser PWA, not the Electron desktop app. Keep
|
|
these limitations explicit in UI, troubleshooting, and release notes:
|
|
|
|
- EPG/XMLTV is not supported in the PWA yet. Do not render live EPG panels,
|
|
programme-guide entry points, or EPG-fetching flows in browser/PWA mode, and
|
|
do not use EPG as the readiness signal for Docker.
|
|
- The PWA does not use the Electron SQLite database or DB worker. Playlist
|
|
metadata uses `PlaylistsService` with IndexedDB, which
|
|
`PlaylistStorageBackend` selects when no SQLite bridge exists; Xtream
|
|
favorites, recently viewed items, playback positions, and cached collection
|
|
snapshots use `PwaXtreamDataSource` browser storage.
|
|
- Browser playlist deletion must go through `PlaylistsService`, not
|
|
`DatabaseService`. `PlaylistsService.deletePlaylist()` runs registered
|
|
cleanup hooks such as the PWA Xtream cleanup so localStorage sidecar data does
|
|
not survive after the source is removed.
|
|
- The Docker/PWA runtime cannot launch MPV, VLC, IINA, Embedded MPV, download
|
|
manager flows, or Electron remote-control features. If inline browser
|
|
playback fails, the supported browser fallback is copying the stream URL and
|
|
opening it manually in an external player.
|
|
|
|
Provider URLs are registered before proxy calls so the proxy endpoints do not
|
|
accept raw target URLs in query strings. Registration validates the target URL
|
|
before any outbound request:
|
|
|
|
- only `http:` and `https:` provider URLs are accepted
|
|
- URL credentials are rejected
|
|
- loopback, private, link-local, and reserved network targets are blocked by
|
|
default
|
|
- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` explicitly enables trusted
|
|
local/LAN targets for development, mock servers, or private deployments
|
|
|
|
### Provider redirects and connection policy
|
|
|
|
`ValidatedHttpClient` applies the registration URL policy again before every
|
|
outbound request on `/xtream`, `/stalker`, `/parse`, and `/parse-xml`, including
|
|
the initial request. Axios automatic redirects are disabled (`maxRedirects: 0`);
|
|
301, 302, 303, 307 and 308 are followed manually, with at most five redirects.
|
|
Relative `Location` values resolve against the URL actually sent, including its
|
|
query. Missing/malformed locations, repeated URLs (ignoring fragments), and an
|
|
exhausted redirect budget fail without dispatching another request. Intermediate
|
|
bodies are destroyed at headers; malformed or endless redirect bodies do not
|
|
block following an otherwise valid location. Final bodies retain a native socket
|
|
inactivity timer: axios stream mode stops its own timeout handling at headers,
|
|
so `provider-response.ts` keeps that timer active until consumption finishes.
|
|
A body failure preserves received HTTP status evidence; cancellation remains
|
|
inconclusive before any redirect.
|
|
|
|
Every DNS answer must be a valid permitted IP. Mixed public/private answers
|
|
fail closed. The strict policy rejects private/reserved IPv4, IPv4-mapped private
|
|
IPv6, and IPv6 outside global unicast `2000::/3`, plus protocol-assignment,
|
|
documentation and 6to4 ranges within it. These deliberately conservative ranges
|
|
follow the [IANA IPv6 address space](https://www.iana.org/assignments/ipv6-address-space/)
|
|
and [special-purpose registry](https://www.iana.org/assignments/iana-ipv6-special-registry/).
|
|
The LAN opt-in applies to the whole
|
|
chain; it still requires HTTP(S), no URL userinfo and concrete DNS addresses.
|
|
|
|
Fresh HTTP/HTTPS agents pin socket lookup to the exact validated IPv4/IPv6
|
|
answer set for that hop. No second DNS query or pooled connection may substitute
|
|
an unvalidated address. The default axios transport uses a fixed logical hostname (`provider.invalid`)
|
|
so its connection authority cannot come from user-controlled URL metadata;
|
|
the pinned lookup alone selects an IP. Its native request-options adapter sets
|
|
`path` separately, so even `//host/path` cannot replace the connection authority.
|
|
It also owns the deadline from dispatch through response headers, since axios's
|
|
built-in connection timer only covers its own native transport objects. The original provider host and port are
|
|
explicitly preserved in Host, TLS SNI and certificate identity checks (literal
|
|
IPs omit SNI but still verify the original IP). Axios proxies and Node environment proxies
|
|
are disabled for these requests so a proxy cannot independently resolve the
|
|
origin. Deployments that require an outbound HTTP proxy must use a different
|
|
network arrangement; setting `HTTP_PROXY`/`HTTPS_PROXY` does not route provider
|
|
requests through it. This guarantee concerns address selection in the Node
|
|
transport, not routing/NAT performed outside the process. Private-network
|
|
opt-in intentionally relaxes address restrictions.
|
|
|
|
Original axios `params` are applied only to the initial request. Subsequent
|
|
queries come from `Location` resolution, without appending the original
|
|
credentials again. Authorization, Cookie, Proxy-Authorization and Stalker SN
|
|
headers are retained only on the same origin; changes of host, port or scheme
|
|
(including HTTPS downgrades) strip them for the rest of the chain. User-Agent
|
|
and other non-secret protocol headers survive. Providers can explicitly put
|
|
query values in `Location`; the backend does not rewrite provider-issued URLs.
|
|
|
|
Policy errors contain fixed messages/statuses without URLs, DNS exceptions or
|
|
transport objects. Portal routes keep HTTP 200 with a `{ message, status }`
|
|
error envelope; playlist/XMLTV routes use the actual error status. Registration
|
|
continues to use real HTTP error statuses. See
|
|
[host connectivity guard](host-connectivity-guard.md) for chain ownership and
|
|
failure attribution.
|
|
|
|
Do not disable TLS certificate validation in the backend proxy. For private
|
|
certificate authorities, configure Node with `NODE_EXTRA_CA_CERTS`.
|
|
|
|
## PWA Portal User Data
|
|
|
|
Xtream favorites and recently viewed items use the browser-side
|
|
`PwaXtreamDataSource` when Electron DB preload APIs are unavailable. The
|
|
`XTREAM_DATA_SOURCE` provider chooses the Electron SQLite-backed source only
|
|
when `RuntimeCapabilitiesService.supportsXtreamSqliteDataSource` is true; a
|
|
browser PWA or partial preload bridge must fall back to the PWA source and run
|
|
the browser cleanup hook on playlist deletion. The PWA stores this user activity
|
|
and sidecar state in localStorage:
|
|
|
|
- `xtream-collection-items`
|
|
- `xtream-favorites`
|
|
- `xtream-recent-items`
|
|
- `xtream-playlists`
|
|
- `xtream-playback-positions`
|
|
|
|
`PwaXtreamDataSource` and `PwaCollectionStorage` reach these keys through the
|
|
`USER_DATA_STORAGE` token from `@iptvnator/services` (default:
|
|
`localStorage`), never through `localStorage` directly, so a deployment can
|
|
substitute a server-synced storage.
|
|
|
|
Entries should include a content snapshot when the item is added. Global
|
|
collection routes and the dashboard can then restore titles, posters, content
|
|
type, and category IDs after navigation or a page reload without relying on the
|
|
Electron SQLite content table.
|
|
|
|
Shared collection services that need Xtream favorites or recent data should use
|
|
`XTREAM_DATA_SOURCE` from `@iptvnator/portal/xtream/data-access` from a
|
|
`type:data-access` or `type:feature` boundary. UI libraries in the M3U domain
|
|
must not import Xtream data-access directly; use `PlaylistsService` for source
|
|
metadata changes and let app-level cleanup providers handle portal-specific
|
|
browser sidecar data.
|
|
|
|
## Docker Runtime
|
|
|
|
The Docker image has two stages:
|
|
|
|
1. Build stage installs dependencies and runs `web:pwa` plus `web-backend`.
|
|
pnpm is installed globally at the exact `packageManager` version rather than
|
|
through Corepack, which Node 25 unbundled, so the base-image major stays
|
|
free to move.
|
|
2. Runtime stage uses `node:24-alpine` with nginx installed. nginx serves
|
|
`dist/apps/web` and proxies `/api/*` to the local Express backend.
|
|
The entrypoint renders the nginx config from a `${PORT}` template, starts the
|
|
backend, waits for `/health`, and then starts nginx. If either process exits
|
|
after startup, the entrypoint exits the container so the compose restart
|
|
policy can recover the service.
|
|
|
|
Default runtime values:
|
|
|
|
- `BACKEND_URL=/api`
|
|
- `CLIENT_URL=http://localhost:4333`
|
|
- `PORT=3000`
|
|
- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=0`
|
|
- `NODE_EXTRA_CA_CERTS` unset
|
|
- `IPTVNATOR_STORAGE=server`, `IPTVNATOR_DATA_DIR=/data` (a `VOLUME`)
|
|
- `IPTVNATOR_ACCESS_TOKEN` unset, `IPTVNATOR_MAX_DOCUMENT_MB=64`
|
|
- `IPTVNATOR_BACKEND_START_TIMEOUT=300`: seconds the entrypoint waits for
|
|
`/health`; the backend migrates its database before it listens
|
|
|
|
better-sqlite3 is a native addon, so the Dockerfile builds it in a separate
|
|
`native` stage that is not pinned to `$BUILDPLATFORM`; multi-arch builds
|
|
produce it for each target platform. nginx raises `client_max_body_size` to
|
|
64 MB for `/api/` and gzips JSON responses.
|
|
|
|
When hosting behind another domain, set `CLIENT_URL` to the browser origin and
|
|
keep `BACKEND_URL=/api` unless the reverse proxy exposes the backend elsewhere.
|
|
Only set `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=1` when the self-hosted
|
|
instance is restricted to trusted users and intentionally needs private network
|
|
IPTV targets. For providers using private certificate authorities, mount the CA
|
|
bundle into the container and set `NODE_EXTRA_CA_CERTS` to that mounted path.
|
|
|
|
## Validation
|
|
|
|
Use the narrow validation ladder for self-hosted changes:
|
|
|
|
```bash
|
|
pnpm nx test web-backend
|
|
pnpm nx test web-backend --testPathPatterns=storage
|
|
pnpm nx test web --runTestsByPath apps/web/src/app/services/runtime-config.spec.ts
|
|
pnpm nx build web --configuration=pwa --skip-nx-cache
|
|
pnpm nx build web-backend
|
|
pnpm nx run web-e2e:e2e -- --project=chromium --grep @self-hosted
|
|
docker compose -f docker/docker-compose.yml config
|
|
```
|
|
|
|
Run `docker build -t iptvnator:self-hosted-test -f docker/Dockerfile .` when a
|
|
Docker daemon is available.
|
|
|
|
For manual Docker smoke testing, run the Xtream and Stalker mock servers plus a
|
|
small M3U fixture, then verify in the browser that M3U, Xtream, and Stalker can
|
|
add sources, play an item, toggle favorites, populate global favorites,
|
|
populate recently viewed, and appear on the dashboard rails.
|
|
|
|
## Service factory and build bases
|
|
|
|
`DataService` in `libs/services/src/lib/data.service.ts` is the renderer service
|
|
contract. `DataFactory()` in `apps/web/src/app/app.config.ts` chooses
|
|
`ElectronService` for the desktop bridge and `PwaService` for browser HTTP and
|
|
IndexedDB work. This environment-level selection is not evidence for an
|
|
individual capability: Xtream data-source selection requires its complete
|
|
SQLite bridge, and feature visibility follows `RuntimeCapabilitiesService`.
|
|
The same workspace route tree is used in both runtimes.
|
|
|
|
Desktop relational data uses the canonical schema/connection in
|
|
`libs/shared/database`; the SQLite path is `~/.iptvnator/databases/iptvnator.db`.
|
|
Desktop Chromium settings/storage still exist alongside SQLite. PWA data uses
|
|
browser IndexedDB (with browser quotas); its structure is not the SQL schema.
|
|
Browser-selected file uploads remain possible even though native filesystem
|
|
access is Electron-only.
|
|
|
|
Web development and PWA use `baseHref="/"`; packaged Electron frontend uses
|
|
`baseHref="./"` so file URLs resolve. In `apps/web/project.json`, `production`
|
|
is the Electron frontend build, `pwa` is the web build, and `development` uses
|
|
the index base. Do not ship the Electron frontend build as a PWA deployment.
|