Fix region loader to find the manifest in prod (dist/) — dev-vs-prod path (v0.20.0, completes #16 P1)

v0.19.0's region-level ownership did not activate in the production image:
server/regions.ts loadRegions only looked in public/, but the Docker image keeps
only dist/ (Vite copies public/* -> dist/ at build), so the loader found nothing
and expandCountriesToKeys degraded to a no-op — fresh games kept raw ISO_A3 codes
instead of region keys (the client's own /region-keys.json fetch still worked,
since the client is served from dist/).

loadRegions now probes public/ (dev) then dist/ (prod, == clientDir) and uses the
first directory holding both files. A new test covers the prod-only (dist/), dev
(public/), and empty-root (no data) layouts.
This commit is contained in:
2026-10-14 09:32:29 +00:00
parent 99ee93f6da
commit cf7d3cb122
2 changed files with 74 additions and 11 deletions
+23 -10
View File
@@ -1,7 +1,9 @@
// Region-key data access (server). Loads the deterministic region-key manifest
// (public/region-keys.json: stable feature id -> region key) and the sub-national
// geo (public/regions.geo.json: feature id -> { name, parent }), then joins them
// into the lookups the region-level ownership model (v0.19, #16 P1) needs:
// (region-keys.json: stable feature id -> region key) and the sub-national geo
// (regions.geo.json: feature id -> { name, parent }) — from public/ in dev (Vite
// serves it) or dist/ in prod (Vite copies public/* -> dist/ at build, and the
// Docker image keeps only dist/) — and joins them into the lookups the
// region-level ownership model (v0.19, #16 P1) needs:
//
// - expand a nation's ISO_A3 country list -> the region keys of all its sub-regions
// - build the LLM "regions" reference table (key -> "name (parent ISO_A3)")
@@ -35,18 +37,29 @@ interface RegionData {
let data: RegionData | null = null;
/**
* Load (or reload) the region-key manifest + geo from `root`. Idempotent.
* Returns true when both files loaded cleanly. A missing/broken file clears the
* data and returns false, so the server still boots and degrades gracefully
* (expansion becomes a no-op, ownership falls back to the raw input keys).
* Load (or reload) the region-key manifest + geo. Idempotent. The two files are
* served to the client from `public/` in dev (Vite serves it directly) and from
* `dist/` in prod (Vite copies `public/*` → `dist/` at build and the Docker image
* keeps only `dist/`), so probe `public/` then `dist/` (plus the root as a last
* resort) and use the first directory that holds both files. Returns true when
* both loaded cleanly; a missing/broken pair clears the data and returns false
* so the server still boots and degrades gracefully (expansion becomes a no-op,
* ownership falls back to the raw input keys).
*/
export function loadRegions(root: string): boolean {
const keyPath = path.join(root, 'public', 'region-keys.json');
const geoPath = path.join(root, 'public', 'regions.geo.json');
if (!existsSync(keyPath) || !existsSync(geoPath)) {
const keyFile = 'region-keys.json';
const geoFile = 'regions.geo.json';
const base = [
path.join(root, 'public'), // dev: Vite serves public/ at /
path.join(root, 'dist'), // prod: Vite copies public/* -> dist/ (== clientDir)
root, // last resort
].find((d) => existsSync(path.join(d, keyFile)) && existsSync(path.join(d, geoFile)));
if (!base) {
data = null;
return false;
}
const keyPath = path.join(base, keyFile);
const geoPath = path.join(base, geoFile);
try {
const idToKey = JSON.parse(readFileSync(keyPath, 'utf8')) as Record<string, string>;
const geo = JSON.parse(readFileSync(geoPath, 'utf8')) as {