[orchestrator] round 7 phase 0: 27.3 MB -> 7.0 MB, and the build can no longer forget

All 27 shipped GLBs run through gltf-transform (meshopt + webp + 1024 textures).
Measured per-asset, no failures: 27,304 KB -> 7,132 KB (74% off) with PBR intact.
The biggest wins are the wave-1/wave-2 assets that shipped raw (artifact-bottler
1453->329, chroma-keyer 1928->301, mv-extractor 1376->190); the six baked through
the new farm script were already lean and barely moved, which is the check that
the farm-side step is doing its job.

tools/optimize-assets.mjs: idempotent gate wired into deploy.sh BEFORE vite build.
Skips anything already carrying EXT_meshopt_compression (so it costs ~0s when
nothing is new) and FAILS THE BUILD on any asset over 900 KB — a farm regression
that emits trellis2-dense meshes now stops the deploy instead of shipping 36 MB.

deploy.sh verify gap closed: it tested exactly one .js file, so a deploy that
shipped ZERO models would have reported success. Now checks the JS bundle, a real
GLB endpoint, and asserts live model count == built model count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-29 00:21:20 +10:00
parent 84d378a80e
commit 20d2bcbbfe
29 changed files with 86 additions and 2 deletions

View File

@ -12,6 +12,9 @@ VPS=humanjing@100.71.119.27
CONTAINER=forum-nginx
WEBROOT=/usr/share/nginx/html
echo "— optimize assets (idempotent; skips anything already meshopt-compressed) —"
node fktry/tools/optimize-assets.mjs
echo "— build —"
(cd fktry && npx vite build --base=./)
cp index.html fktry/dist/match.html
@ -27,7 +30,19 @@ ssh "$VPS" "docker exec $CONTAINER rm -rf $WEBROOT/glytch \
echo "— verify (cache-busted) —"
curl -fsS -o /dev/null -w '%{http_code} https://partly.party/glytch/\n' "https://partly.party/glytch/?v=$(date +%s)"
curl -fsS -o /dev/null -w '%{http_code} https://partly.party/glytch/match.html\n' "https://partly.party/glytch/match.html?v=$(date +%s)"
# data-endpoint check, not just index (classic nginx-routing failure mode):
# Data-endpoint checks, not just the index (classic nginx-routing failure mode).
# Round-7 fix: this used to test ONE js file, so a deploy that shipped zero models
# still reported success. Now the JS bundle, a real GLB, and the model COUNT.
ASSET=$(ls fktry/dist/assets | grep -m1 '\.js$')
curl -fsS -o /dev/null -w "%{http_code} https://partly.party/glytch/assets/$ASSET\n" "https://partly.party/glytch/assets/$ASSET?v=$(date +%s)"
curl -fsS -o /dev/null -w "%{http_code} assets/$ASSET\n" "https://partly.party/glytch/assets/$ASSET?v=$(date +%s)"
MODELS_LOCAL=$(ls fktry/dist/assets/models/*.glb 2>/dev/null | wc -l | tr -d ' ')
MODEL=$(basename "$(ls fktry/dist/assets/models/*.glb 2>/dev/null | head -1)")
if [ -z "$MODEL" ] || [ "$MODELS_LOCAL" -eq 0 ]; then
echo "FAIL: build produced no GLB models" >&2; exit 1
fi
curl -fsS -o /dev/null -w "%{http_code} assets/models/$MODEL\n" "https://partly.party/glytch/assets/models/$MODEL?v=$(date +%s)"
MODELS_LIVE=$(ssh "$VPS" "docker exec $CONTAINER sh -c 'ls $WEBROOT/glytch/assets/models/*.glb 2>/dev/null | wc -l'" | tr -d ' ')
echo "models: $MODELS_LIVE live / $MODELS_LOCAL built"
[ "$MODELS_LIVE" = "$MODELS_LOCAL" ] || { echo "FAIL: model count mismatch" >&2; exit 1; }
echo "done. If this was a major change: purge Cloudflare cache for partly.party."

Binary file not shown.

View File

@ -0,0 +1,69 @@
#!/usr/bin/env node
/**
* Idempotent GLB optimization gate. Runs from deploy.sh BEFORE vite build.
*
* Why this exists: the farm emits 18 MB PBR GLBs. Shipped raw, 27 of them were
* 27.3 MB; through `gltf-transform optimize` they are 7.0 MB with the same PBR.
* That step used to live in my head, which is not a build system. An asset added
* by hand or by a farm run that skipped the post-step would silently ship raw.
*
* Skips anything already carrying EXT_meshopt_compression, so it is safe to run
* every deploy and costs ~0s when there is nothing new.
*/
import { execFileSync } from 'node:child_process';
import { readdirSync, readFileSync, statSync, renameSync, rmSync } from 'node:fs';
import { join } from 'node:path';
const DIR = new URL('../public/assets/models/', import.meta.url).pathname;
const MAX_KB = 900; // per-asset ceiling; a farm regression should fail the build
/** Read the GLB JSON chunk without a glTF library. */
function meta(path) {
const b = readFileSync(path);
if (b.length < 20 || b.subarray(0, 4).toString() !== 'glTF') return null;
const jsonLen = b.readUInt32LE(12);
try {
return JSON.parse(b.subarray(20, 20 + jsonLen).toString());
} catch {
return null;
}
}
const files = readdirSync(DIR).filter((f) => f.endsWith('.glb'));
let optimized = 0, skipped = 0, before = 0, after = 0;
const oversize = [];
for (const f of files) {
const p = join(DIR, f);
const sizeBefore = statSync(p).size;
before += sizeBefore;
const m = meta(p);
if (m?.extensionsRequired?.includes('EXT_meshopt_compression')) {
skipped++;
after += sizeBefore;
} else {
const tmp = `${p}.opt`;
execFileSync('npx', ['--yes', '@gltf-transform/cli@latest', 'optimize', p, tmp,
'--texture-compress', 'webp', '--texture-size', '1024',
'--simplify-error', '0.002', '--compress', 'meshopt'],
{ stdio: 'pipe', timeout: 900_000 });
rmSync(p);
renameSync(tmp, p);
optimized++;
after += statSync(p).size;
console.log(` optimized ${f}: ${(sizeBefore / 1024) | 0} KB -> ${(statSync(p).size / 1024) | 0} KB`);
}
const kb = (statSync(p).size / 1024) | 0;
if (kb > MAX_KB) oversize.push(`${f} (${kb} KB)`);
}
console.log(`assets: ${files.length} files, ${optimized} optimized, ${skipped} already lean — ` +
`${(before / 1048576).toFixed(1)} MB -> ${(after / 1048576).toFixed(1)} MB`);
if (oversize.length) {
console.error(`\nFAIL: ${oversize.length} asset(s) over ${MAX_KB} KB after optimization:`);
for (const o of oversize) console.error(` ${o}`);
console.error('A mesh this heavy usually means the bake used too dense a pipeline ' +
'(trellis2 1024_cascade produces ~487k tris; hunyuan3d_mlx is the machine default).');
process.exit(1);
}