Skip to content

[ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ - #704

Merged
ddol merged 5 commits into
mainfrom
claude/embed-all-web-build
Oct 7, 2026
Merged

ddol merged 5 commits into
mainfrom
claude/embed-all-web-build

Conversation

@ddol

@ddol ddol commented Oct 6, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two things about what the Go binary embeds from the web build. It also finishes items 1.1 and 1.3 of the binary size reduction plan.

  1. //go:embed all:web/build. The old web/build/* kept web/build/_app because the glob names it, but silently skipped every file below it whose name starts with _ or .. SvelteKit names shared chunks <hash>.js, and the hash alphabet includes _ (D_9rjNUa, start.BQyT_uoz.js). A chunk whose hash began with one would be missing from the binary and 404 in production, while --debug (which serves from disk) worked. go list with a fake _Ab12Cd3.js confirmed it: the old directive dropped the file, the new one keeps it.
  2. /favicon.ico was a 404 in production. static/ is no longer embedded or written. StaticFiles (static/*) existed only for /favicon.ico, but the handler served http.FS(StaticFiles) without fs.Sub, so it looked for favicon.ico at the root of a tree whose files sit under static/. A long-running local server answers 404 there today. The app links /app/favicon.ico itself, but /docs/ declares no icon, so browsers hit the broken route. The favicon now comes from the web build: embedded in production, ./web/build in dev. web/scripts/copy-build.js and its postbuild hook only existed to feed that embed, so they go, along with the tracked static/favicon.ico (byte-identical to web/static/favicon.ico).

Size

Smaller than the plan's history suggests. The compiler stores embedded file contents content-addressed, so static/'s byte-identical copy of the web build (35 files, 1,820,699 bytes) was stored once. Removing the embed shrank a local darwin/arm64 make build-radar-local binary by 16,752 bytes. What static/ still cost was content that no longer matched the build: 3 stale pages (31 KB) in one long-lived checkout, and at worst the 172 MB of accumulated builds the plan originally measured. That can't recur now, because nothing reads or writes static/.

Commits

  1. [ai][go] embed all of web/build, including _-prefixed files: TestWebBuildEmbedKeepsUnderscoreFiles pins the directive. No committed build contains a _ file to prove the fix at run time. TestWebBuildEmbedMatchesDisk checks every on-disk build file is embedded; CI's integration job runs it against a real build. Both fail with the old directive.
  2. [ai][go] serve /favicon.ico from the web build and stop embedding static/: TestStart_ProductionModeListenAndShutdown already requested /favicon.ico without checking the response. It now requires a 200 with web/build/favicon.ico's bytes, or a 404 for the index-only stub build, and it fails against the old handler. TestStart_DevModeWithBuildDir checks the dev path.
  3. [ai][js][make][cfg] stop copying the web build into static/: removes copy-build.js, postbuild and the tracked favicon. make clean-web now removes static/ outright. .gitignore keeps static/ ignored, with a note, so old checkouts' leftovers stay quiet. The static-build Docker context drops !static/.
  4. [ai][docs] binary size plan: phase 1.1 and 1.3 landed: ticks the checklist, records the measurements, and turns two links to the now-absent static/ into code spans. It also updates the canonical hub and ARCHITECTURE.md.
  5. [ai][docs] physical pose plan: British spelling of serialisation: unrelated one-word fix, so make lint-docs passes again.

Verification

All of the following were re-run after rebasing onto main at #685, on Go 1.27.1.

  • make format-go, make lint-go, make test-go (84 packages) pass. The new and changed tests pass under -tags=pcap.
  • make lint-web, make test-web (909) pass. make build-web passes and leaves no static/.
  • make build-radar-local builds a stamped binary.
  • The production favicon assertion was exercised both ways: 200 with a real build, 404 with the favicon moved out of web/build.
  • check-backtick-paths.py, check-relative-links.py and check-plan-canonical-links.py pass with static/ absent.
  • make lint-docs passes. On main it failed on one British-spelling hit, serialization in docs/plans/lidar-swift-physical-pose-plan.md (from [ai][docs] devlog register format and PR numbers; 0.5.2 backlog cleanup #677), which commit 5 fixes.

After merging, run make clean-web once in existing checkouts to delete the leftover static/.

🤖 Generated with Claude Code

ddol and others added 5 commits October 6, 2026 15:29
WebBuildFiles was `//go:embed web/build/*`. The glob names web/build/_app,
so that directory is kept, but Go then skips every file below it whose
name starts with "_" or ".". SvelteKit names shared chunks
_app/immutable/chunks/<hash>.js, and the hash alphabet includes "_"
(D_9rjNUa, start.BQyT_uoz.js). A chunk whose hash started with one would
be missing from the binary and 404 under /app in production, while the
--debug server, which reads web/build from disk, served it. Today's
single-chunk build has one hash-named file per build, so the odds were
small but never zero. The SvelteKit 3 split strategy would make dozens.

`all:web/build` keeps everything, as DocsSiteFiles already does. With
a stub build plus fake chunks _Ab12Cd3.js and Bx9_k2Lm.js, `go list`
showed the old directive embedding only the second; the new one embeds
both. StaticFiles (`static/*`) keeps its pattern: the server routes only
/favicon.ico to it.

TestWebBuildEmbedKeepsUnderscoreFiles pins the directive itself, since
no committed build contains a "_" file to prove the point at run time.
TestWebBuildEmbedMatchesDisk checks that every file under web/build is
readable from the embed, which CI's integration job runs against a real
build. With the old directive and the fake chunk on disk, both fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tic/

StaticFiles (`//go:embed static/*`) existed for one route, /favicon.ico,
and in production that route had been a 404: the handler was
http.FileServer(http.FS(radar.StaticFiles)), which looks for favicon.ico at
the root of a tree whose files sit under static/. Dev mode read ./static
from disk and worked, which is why it went unnoticed. The SvelteKit app
links /app/favicon.ico itself, but /docs/ declares no icon, so browsers
fell back to the broken /favicon.ico. The long-running local server
answers 404 there today.

The favicon now comes from the web build, web/static/favicon.ico by way of
web/build: fs.Sub of WebBuildFiles in production and ./web/build in dev,
the same tree /app is served from. This is items 1.1 and 1.3 of the
binary size reduction plan. The two icons are byte-identical, so nothing
visible changes except that the route works.

Dropping StaticFiles also stops the binary embedding whatever sits in
static/, which web/scripts/copy-build.js filled with a copy of the web
build. Identical files are stored once by the compiler, so in a local
build the binary shrinks by only 16,752 bytes. The cost was stale pages
that no longer matched the build (31 KB of them in a long-lived checkout),
and once the 172 MB of accumulated builds the plan measured.

TestStart_ProductionModeListenAndShutdown already requested /favicon.ico
but checked nothing, and it fails against the old handler with a 404. It
now requires 200 and web/build/favicon.ico's bytes, or a 404 when the
build is the index.html-only stub. TestStart_DevModeWithBuildDir checks
that dev mode serves the build's favicon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web build's postbuild hook, web/scripts/copy-build.js, copied
web/build/* into the repo-root static/ so the server could embed it. #49
added the copy when /app was already served from web/build, and the
server never served anything from static/ but /favicon.ico. With the
favicon now taken from the web build, nothing reads static/, so the
hook and the script go, and so does the tracked static/favicon.ico,
which is byte-identical to web/static/favicon.ico.

- make clean-web removed three static/_app subdirectories; it now
  removes static/ outright, clearing what older builds left in existing
  checkouts.
- .gitignore keeps static/ ignored so those leftovers stay out of git
  status, and says why.
- The static-build Docker context no longer re-includes static/.

`make build-web` now leaves no static/ behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ticks the plan's first three checklist items: the static/ embed and
StaticFiles are gone, and the favicon comes from the web build in
production and dev. A note at the top records what was measured on the
way. static/ held a byte-identical copy of the web build, which the
compiler's content-addressed storage kept almost free (16,752 bytes), plus
stale pages. The favicon it was kept for had been a 404 in production.

The two Markdown links to ../../static become code spans, since a clean
checkout no longer has that directory. The canonical hub's "currently
~211 MB" becomes "when the plan was written", and the stale static/ row
reads 0 MB. ARCHITECTURE.md says where /favicon.ico comes from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#677 left "serialization" in the plan's effort table, which fails the
British-spelling step of `make lint-docs` on main. Same length, so the
table stays aligned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 22:47
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/api/server.go 87.50% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@ddol
ddol merged commit 21722c3 into main Oct 7, 2026
22 of 23 checks passed
@ddol
ddol deleted the claude/embed-all-web-build branch October 7, 2026 00:16
ddol added a commit that referenced this pull request Oct 7, 2026
…tatic 4 (#705)

* [ai][js][cfg] web: import src/lib through #lib subpath imports

SvelteKit 3 removes the $lib alias in favour of a Node subpath import
declared in package.json "imports". Vite, TypeScript and Jest already
resolve that on SvelteKit 2, so the rename lands first, on its own, and
the kit 3 migration that follows carries only the changes that need
judgement.

The 34 files follow the rule `sv migrate sveltekit-3` applies: `$lib/foo`
becomes `#lib/foo.js` when foo is a .ts module (TypeScript's convention
for naming a .ts source), and a .svelte import keeps its extension. The
extension is not optional: TypeScript rejects an extensionless
`#lib/foo` with TS2307. Jest maps `#lib/foo.js` back to src/lib/foo and
lets moduleFileExtensions find the .ts.

On SvelteKit 2.70.3: svelte-check 0 errors, 906 tests pass, lint clean,
build passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][js][cfg] web: migrate to SvelteKit 3 and adapter-static 4

Dependabot's kit 3.0.0 bump (#681) failed the web build and #693 held kit
at 2.x. This is the migration that bump needed.

Config. Kit 3 refuses svelte.config.js, so its options move into
sveltekit({...}) in vite.config.ts, flat rather than under `kit:`; the
note on why $scene is a Vite alias and not a kit alias moves with them.
tsconfig.json extends $app/tsconfig, which svelte-kit sync writes into
node_modules and which already sets allowJs, checkJs, bundler resolution
and the rest, so only strict and sourceMap remain. That parent has no
include list, and kit's suggested "*" would put jest.config.js and the
.cjs transformer under checkJs, so the include is src plus
vite.config.ts, which kit 2's generated config also checked.

Bundling. Kit 3 sets codeSplitting itself and ignores the
manualChunks: () => 'everything.js' that #25 added ("bundle chunks,
improve go server log-lines"; #25 served web/build from disk, and the
embed came in #30). output.bundleStrategy: 'single' is kit's option for
the same result: one bundle.<hash>.js and one bundle.<hash>.css, the CSS
byte-identical to kit 2's everything.<hash>.css. It keeps the shipped
shape of the kit 2 build. Kit's split default would also serve correctly
now: since #704 the server embeds all of web/build, so a chunk whose hash
begins with "_" is no longer dropped from the binary. Switching to split
is a separate decision about load behaviour on the Pi. emptyOutDir goes:
kit overrides it, and adapter-static clears build/ itself.

Build provenance. Kit 3 fills %sveltekit.env.NAME% in app.html only for
variables declared in src/env.ts. Without one, the app-git-sha,
app-build-time and app-build-version meta tags render empty, and
`make dev-go` would rebuild web on every run, since its staleness check
greps app-build-version. src/env.ts declares the three as public and
static, and required: set-build-env.js sets all three for every build
and dev run, and a bare `vite build` now fails naming the missing ones.

Runtime APIs. $app/environment is now $app/env. $app/stores is gone, and
the two pages that used it read $app/state through svelte/store's
toStore: they use legacy `$:` statements, which never see $app/state
change. Read directly, a saved new scene would leave the reused
/scene/[id] component on 'new', so a second Save would create a second
scene, and the tracks page would ignore a changed sensor_id. page.url is
now a ReadonlyURL, so the layout gives svelte-ux's NavItem (typed for
URL, reads only the pathname) a copy. resolve() treats a leading "/" as
a route ID, so dynamic links pass params: resolve('/scene/[id]', { id }).

engines.node is >=22.17, kit 3's own floor. CI's setup-node "22" and the
ubuntu-24.04 runner image (Node 22.23.3) both satisfy it.

Verified: svelte-check 0 errors, the web tests, lint, make build-web, and
a smoke test of the embedded build under /app on a scratch port covering
every route, client-side navigation, scene create then update (one POST,
then a PUT), and a sensor_id change on the tracks page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][ci] dependabot: let SvelteKit and adapter-static majors through again

#693 ignored semver-major updates of @sveltejs/kit and
@sveltejs/adapter-static until the web app was migrated to kit 3. It now
is, so the hold goes and Dependabot will raise kit 3.x and
adapter-static 4.x bumps as usual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][make][sh] web tooling after SvelteKit 3: watch vite.config.ts, ask for Node 22.17

ensure-dev-web-build did not treat a newer vite.config.ts as making the
web build stale. The SvelteKit config now lives there (svelte.config.js,
which it did not watch either, is gone), so it does.

dev-setup.sh asked for Node.js 18 and offered the NodeSource 20.x
installer; the web app needs 22.17, which kit 3 requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][cfg] web: drop the minimumReleaseAgeExclude entry for kit 2.70.2

#542 exempted @sveltejs/kit@2.70.2 from pnpm's minimum release age so
that bump could install the day it shipped. The lock has since moved to
2.70.3 and now to 3.0.0, so the exemption names a version nothing
resolves to. The lockfile does not record the setting, and
`pnpm install --frozen-lockfile` is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [ai][js][cfg] web: mark the two dynamic routes client-only so prerendering is clean

Every build printed prerender_unseen_routes for /scene/[id] and
/site/[id]. Kit 3 leads with the error code, so it now reads like a
failure, but kit 2 printed the same thing; handleUnseenRoutes: 'warn'
(from #49) kept it from failing the build. The root +layout.js marks
every route prerenderable, and kit cannot prerender these two: their ids
are database rows, and with ssr off the prerendered HTML is a bare shell
with no links for the crawler to follow. They work at run time because
the Go server answers any unknown /app path with index.html, the app
shell, and the client router renders the page.

Each route now says so with `export const prerender = false`. With two
routes not prerendered and no adapter fallback page, adapter-static's
strict check would refuse the build, so it gets `strict: false` and a
comment naming the Go server's fallback as the reason that is safe.
handleUnseenRoutes goes back to kit's default, 'fail': with the marker on
site/[id] moved aside, the build now fails naming /site/[id] instead of
passing with a warning.

The build output is unchanged (the same 17 files, the version stamp in
index.html intact). /app/site/1 and /app/scene/<id>, loaded directly
from the embedded build, render their pages from the fallback shell with
no console errors. svelte-check 0 errors, 909 tests, lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants