A friend on Chrome hit "C.subtrees is not iterable" — Chrome was serving
the previous skill-tree JSON shape ({nodes, edges} at top level) out of
the disk cache while running the new bundle that expects {subtrees}.
Hard reload fixed his session but other users could hit the same.
Three layers of defense:
1. backend/data.controller.ts: Cache-Control switches from
'public, max-age=3600' to 'no-cache, must-revalidate' so the browser
always revalidates JSON files against the server.
2. frontend/data.ts: every data fetch gets ?v=2 + cache:'no-cache' to
bust any existing browser/CDN entry.
3. frontend/data.ts loadSkillTree(): if the response still has the legacy
shape (no subtrees, but nodes array), wrap it in a single anonymous
subtree so the UI doesn't crash on iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Controller,
|
|
Get,
|
|
Header,
|
|
NotFoundException,
|
|
Param,
|
|
Res,
|
|
} from '@nestjs/common';
|
|
import type { Response } from 'express';
|
|
import { createReadStream, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const DATA_ROOT = process.env.DATA_ROOT || join(__dirname, '..', '..', 'data');
|
|
|
|
// Allow-list of known filenames — guards against path traversal.
|
|
const ALLOWED = new Set<string>([
|
|
'index.json',
|
|
'character-xp.json',
|
|
'spec-combat.json',
|
|
'spec-crafting.json',
|
|
'spec-exploration.json',
|
|
'spec-gathering.json',
|
|
'spec-sabotage.json',
|
|
'faction-atreides.json',
|
|
'faction-harkonnen.json',
|
|
'skills-benegesserit.json',
|
|
'skills-mentat.json',
|
|
'skills-planetologist.json',
|
|
'skills-swordmaster.json',
|
|
'skills-trooper.json',
|
|
]);
|
|
|
|
@Controller('data')
|
|
export class DataController {
|
|
@Get(':file')
|
|
@Header('Content-Type', 'application/json; charset=utf-8')
|
|
// 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');
|
|
}
|
|
const path = join(DATA_ROOT, file);
|
|
if (!existsSync(path)) {
|
|
throw new NotFoundException();
|
|
}
|
|
createReadStream(path).pipe(res);
|
|
}
|
|
}
|