One missing texture blacked out the whole gallery
2026-09-01
A card image that was never generated threw through Suspense and unmounted the entire WebXR scene. The fix is a per-tile boundary, a generator case, and a test that walks every record.
The split-view pane on /projects/spatial-gallery rendered black in production. The pane embeds /xr, and /xr itself was broken: a new writing record, data/writing/13_ai-native-product-development, had no generated card art at public/lab-media/writing/ai-native-product-development.webp. drei's useTexture throws through Suspense when the fetch 404s, nothing caught it, and the uncaught error unmounted the entire Canvas. One missing image file, 32,846 bytes once it was finally generated, took down every project, talk and article in the gallery.
The fix in 5c86f6d is three parts. TileMaterialBoundary, a class component in src/lab/MediaTile.tsx using getDerivedStateFromError, now wraps ImageMaterial so a failed texture costs one tile instead of the scene. Its fallback is a meshBasicMaterial in the card's own accent color, so the tile still reads as a card with its title label rather than disappearing. componentDidCatch logs a warning instead of swallowing it.
The generator was also wrong. scripts/gen-lab-media.sh handled covers written as ./relative paths and as http URLs, but not root-relative public paths, so a record declaring cover: "/og/..." silently produced nothing. A /* case mapping the cover to public$cover was added.
The part that stops this recurring is a test. scripts/content-media.test.mjs now walks data/talks and data/writing, strips the NN_ prefix from each directory, and asserts that every slug has public/lab-media//.webp, or a preview clip pair when src/lab/talkClips.ts lists that slug. The suite is 255 tests and passes.