fix(server-storage): merge user data on move, keep failed writes, sign-in route gate

- A merged playlist gains the browser document's favorites and history;
  Xtream values for one server entry are combined (lists unioned, maps
  merged, the server winning conflicts) and written once.
- A failed user-data write stays queued and survives a re-read; the
  page-hide flush waits for a batch still travelling.
- The start-up screen lets the sign-in route through whatever its check
  reports.
- The backend refuses IPTVNATOR_ACCESS_TOKEN without server storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 00:16:56 +02:00
1 parent 676e3adcd4
commit fc32934ce3
17 files changed
+474 -90

No files matched your search

@@ -1,4 +1,7 @@
import { readServerStorageConfig } from './storage-config';
import {
readServerStorageConfig,
serverStorageConfigProblem,
} from './storage-config';
describe('readServerStorageConfig', () => {
it('keeps storage off and the API open by default', () => {
@@ -42,4 +45,20 @@ describe('readServerStorageConfig', () => {
.maxDocumentBytes
).toBe(64 * 1024 * 1024);
});
it('refuses an access token without server storage', () => {
const problem = (env: Record<string, string>) =>
serverStorageConfigProblem(readServerStorageConfig(env));
expect(problem({ IPTVNATOR_ACCESS_TOKEN: 's3cret' })).toContain(
'IPTVNATOR_STORAGE=server'
);
expect(
problem({
IPTVNATOR_ACCESS_TOKEN: 's3cret',
IPTVNATOR_STORAGE: 'server',
})
).toBeNull();
expect(problem({})).toBeNull();
});
});
@@ -10,6 +10,19 @@ export interface ServerStorageConfig {
const DEFAULT_MAX_DOCUMENT_MB = 64;
const BYTES_PER_MB = 1024 * 1024;
/**
* A combination the browser app cannot use, as a start-up message, or
* `null`. The PWA asks for the access token only in server storage mode,
* so a token on a stateless backend would lock every browser out.
*/
export function serverStorageConfigProblem(
config: ServerStorageConfig
): string | null {
return config.accessToken && !config.enabled
? 'IPTVNATOR_ACCESS_TOKEN needs IPTVNATOR_STORAGE=server: the browser app asks for the token only when the server stores its playlists. Unset the token, or protect a stateless backend in your reverse proxy.'
: null;
}
export function readServerStorageConfig(
env: Record<string, string | undefined> = process.env
): ServerStorageConfig {
+9 -1
View File
@@ -4,7 +4,10 @@ import {
} from '@iptvnator/shared/host-health';
import { closeGracefully } from './app/graceful-shutdown';
import { createWebBackendApp } from './app/web-backend-app';
import { readServerStorageConfig } from './app/storage/storage-config';
import {
readServerStorageConfig,
serverStorageConfigProblem,
} from './app/storage/storage-config';
import {
openServerStorage,
type ServerStorage,
@@ -20,6 +23,11 @@ async function start(): Promise<void> {
}
const config = readServerStorageConfig();
const problem = serverStorageConfigProblem(config);
if (problem) {
console.error(`[web-backend] ${problem}`);
process.exit(1);
}
let storage: ServerStorage | null = null;
if (config.enabled) {
try {