Three ways the compiled-in viewer could be wrong, none of which anything would
have reported. Found by mutation-testing the guard added below — it failed
when it should have passed, and the reason was the second bullet.
* `include_dir!` registers NO rebuild dependency. Run `npm run build` in
frontend/export-viewer, then `cargo build`, and cargo sees no source change
and reuses the cached binary — carrying the PREVIOUS index.html. The file on
disk and the file in the binary disagree, git is clean, every check passes,
and Memories.zip ships a stale viewer. Confirmed empirically: after replacing
the artifact the compiled-in copy did not change until a source file was
touched. A build.rs now declares `rerun-if-changed` for
`static/export-viewer` AND `migrations` — sqlx::migrate!() embeds its
directory the same way, and there the stale snapshot is worse still: the
binary boots against a database that already ran a newer migration and
crash-loops with VersionMissing.
* `emptyOutDir: true` deleted the committed artifact BEFORE generating. That
was safe while the build could not fail; it no longer is, because
`inlineThemeFonts` now calls `this.error` on a keepsake that is not
self-contained. A failed build left the directory empty — and include_dir!
over an empty directory compiles fine, while `write_viewer_with_data`
iterates zero files and returns Ok. The result is a valid archive with every
photo and no viewer. The output is one overwritten file, so nothing
accumulates without the wipe.
* Nothing asserted the viewer was there at all. Now asserted at the point of
use (bail rather than write a viewer-less keepsake) and in a test that checks
presence, plausible size, and that no `url(/...)` survived inlining — the
three ways it can be present but useless.
The Dockerfile copies build.rs with the sources rather than with Cargo.toml, so
the dependency-cache layer stays byte-identical and the dummy build does not
run it.