From e44f27d1cd9ac3216ad542bdf0cfdd63b1ba39a2 Mon Sep 17 00:00:00 2001 From: robot Date: Sun, 9 Aug 2026 00:53:50 +0000 Subject: [PATCH] fix(ssr): inject fresh station config into CSR shell to eliminate flash The CSR shell previously rendered with hardcoded DEFAULT_CONFIG values, then the client-side API call replaced them 50-500ms later, causing a visible 'flash of stale content' (wrong colors, wrong station name). Fix: SSR server fetches live station config from the backend API before serving the CSR shell, injecting it as: - Inline `; +} + +/** + * Build inline CSS custom properties on `:root` so the first paint carries + * the correct color scheme — eliminates the color flash that occurs when + * ThemeService.apply() runs asynchronously after the API call. + */ +function buildThemeStyle(config: StationConfig): string { + const props: string[] = []; + + for (const { field, prop } of COLOR_MAP) { + const value = (config as any)[field]; + if (value && typeof value === 'string') { + props.push(`${prop}: ${value}`); + } + } + + if (props.length === 0) return ''; + + const css = `:root{${props.join(';')}}`; + return ``; +} + +/** + * Fetch fresh station config from the backend API. + * This runs server-side — it goes through the nginx proxy chain to the + * upstream FastAPI backend. On failure we fall back to serving the shell + * unmodified (client-side fetch picks up the slack). + */ +async function fetchStationConfig(): Promise { + // SSR_API_BASE_URL points at nginx (localhost:80) which proxies /api/ to + // the upstream FastAPI backend. This avoids cross-origin issues and reuses + // the same proxy chain the SSR engine itself would use. + const baseUrl = process.env['SSR_API_BASE_URL'] || 'http://localhost:80'; + const url = `${baseUrl}/api/station-config`; + try { - const content = fs.readFileSync(indexPath, 'utf-8'); - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader('X-Render-Mode', 'CSR'); - res.end(content); + const client = baseUrl.startsWith('https') ? https : http; + const data = await new Promise((resolve, reject) => { + const req = client.get(url, { + headers: { 'Accept': 'application/json' }, + }, (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk: Buffer) => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); + res.on('error', reject); + }); + req.on('error', reject); + req.setTimeout(2000, () => { req.destroy(new Error('Station config fetch timed out')); }); + }); + return JSON.parse(data) as StationConfig; + } catch (err) { + console.warn(`[SSR] Failed to fetch station config from ${url}: ${err}`); + return null; + } +} + +/** + * Serve the CSR app shell, enhanced with fresh station config injected at + * runtime so the client bootstraps with real data — no DEFAULT_CONFIG flash. + * + * Strategy: + * 1. Read CSR shell HTML from disk + * 2. Fetch live station config from the backend API + * 3. Inject window.__STATION_CONFIG as an inline