diff --git a/character-builder/backend/src/data/data.controller.ts b/character-builder/backend/src/data/data.controller.ts index ef72505..2e265d5 100644 --- a/character-builder/backend/src/data/data.controller.ts +++ b/character-builder/backend/src/data/data.controller.ts @@ -35,7 +35,10 @@ const ALLOWED = new Set([ export class DataController { @Get(':file') @Header('Content-Type', 'application/json; charset=utf-8') - @Header('Cache-Control', 'public, max-age=3600') + // These files change with the data extractor and schema. A 3600s cache + // bit us once — a browser held an older shape after a schema migration + // and crashed the UI. Force revalidation per request. + @Header('Cache-Control', 'no-cache, must-revalidate') one(@Param('file') file: string, @Res() res: Response) { if (!ALLOWED.has(file)) { throw new BadRequestException('unknown data file'); diff --git a/character-builder/frontend/src/data.ts b/character-builder/frontend/src/data.ts index 563868e..1c2959f 100644 --- a/character-builder/frontend/src/data.ts +++ b/character-builder/frontend/src/data.ts @@ -10,7 +10,11 @@ import type { const API = '/api/data'; async function getJSON(file: string): Promise { - const res = await fetch(`${API}/${file}`); + // cache-bust against stale browser caches that hold an older schema for + // these files. The query string is ignored server-side but defeats any + // long-lived Cache-Control we (or a CDN) may have previously sent. + const url = `${API}/${file}?v=2`; + const res = await fetch(url, { cache: 'no-cache' }); if (!res.ok) throw new Error(`fetch ${file}: ${res.status}`); return res.json() as Promise; } @@ -49,5 +53,23 @@ export async function loadFaction(house: House): Promise { } export async function loadSkillTree(id: ClassId): Promise { - return getJSON(`skills-${id}.json`); + const raw = await getJSON(`skills-${id}.json`); + // Backwards-compat: an older snapshot of this file had {nodes, edges} at + // the top level instead of {subtrees}. Treat it as a single anonymous + // subtree so the UI doesn't blow up on iteration. + if (raw && !raw.subtrees && Array.isArray(raw.nodes)) { + return { + id: raw.id, + name: raw.name, + subtrees: [ + { + name: '', + cols: Math.max(1, ...raw.nodes.map((n: any) => n.col || 1)), + nodes: raw.nodes, + edges: raw.edges || [], + }, + ], + } as SkillTree; + } + return raw as SkillTree; }