- Waitlist signup form on the home page (between Projects and Tools),
cross-posting to the landing site's /api/waitlist (CORS + OPTIONS added there,
gated by WAITLIST_ALLOWED_ORIGINS).
- Version badge gains a pcbjam.com landing-page link (VITE_LANDING_URL).
- Optional Plausible analytics from VITE_PLAUSIBLE_DOMAIN (off by default);
wired into build-demo + both deploy workflows via vars.PLAUSIBLE_DOMAIN.
- Opening a read-only gallery project auto-"moves to local": it forks into a
writable browser-local project at the same slug, shadowing the gallery row.
- Project + local-folder views use the same iconed tool launcher (ToolGrid)
and a navigable directory FileTree instead of a flat file list.
- Boot overlay shows real wasm download progress (Module.instantiateWasm +
streaming byte counter) and a "taking too long" state after 60s.
- Home Libraries section lists the active libs source (the read-only R2/CDN
set in the demo) with a name filter; useLibs now reads libsSourceConfig.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPJYx4urFVhdMjEmYfMCMD
Bumps kicad submodule fef0343 -> 9285d52:
- fix(wasm): lazy footprint library load — no-op enumerateLibrary + on-demand
GetFootprints (mirror of the symbol lazy-load). Fixes the standalone PCB
editor never opening: boot enumerated every global footprint library inline
on the main thread (each a CDN fetch + parse) before first paint, freezing
the tab. Footprints now load on demand (Add-Footprint chooser / footprint
editor tree). eeschema was already lazy; footprint editor boots faster too.
Verified in the local demo: PCB editor and footprint editor both render; the
Add-Footprint chooser lists all ~222 libraries with footprint content fetched
on chooser-open, not at boot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Eu9CbU7EybM8NEjDZD3hq7
After the kicad fork was rebased onto KiCad 10.0.4, GetMajorMinorVersion() (and
thus PATHS::GetUserSettingsPath()'s version subdir) became "10.0", but the
standalone still seeded the global sym-lib-table / config under
~/.config/kicad/kicad/9.99/. The WASM read .../10.0/ and found nothing, so
LIBRARY_MANAGER loaded 0 symbol-table rows and the Add Symbol / Add Power
choosers came up empty ("0 items loaded") with no library list requests.
Aligning KICAD_VERSION_DIR with the build's version makes the chooser load all
222 CDN libraries (19,779 symbols) again. Verified in the local demo against live
R2 (libs/kicad/10.0.3). The footprint editor / pcbnew read the same dir, so this
fixes their libraries too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/deploy/dev-demo.mjs (+ `pnpm dev:demo`) is the dev sibling of
build-demo.mjs: it runs the standalone via `vite dev` in the no-backend demo
configuration so a local WASM build can be exercised against the live R2 CDN.
- libraries: live R2 CDN (VITE_LIBS_SOURCE=cdn, libs/kicad/<lib-tag>, default
10.0.3) — IDB-cached, read-only; the lazy/fat lib-load path
- projects: the read-only example gallery (deploy/demo/gallery.json → the same
"Demo Board") built locally via publish-content.mjs and served same-origin at
/content/<gallery-tag>/; --content-tag pins the live CDN gallery instead;
--no-gallery falls back to local-folder + IDB only
- no partykit (VITE_YJS_PROVIDER=broadcastchannel), no REST backend
(offline.invalid → local-folder loader), browser-local IDB projects
- WASM: local fresh build at /wasm (default) or --wasm r2
The generated gallery (public/content symlink, web/standalone/.demo-cdn) is
gitignored, mirroring the public/wasm dev symlink.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provides wasm/stubs/model_substitution_stub.cpp — no-op IsWrlExtension /
STEP_CATALOG::Build / FindMatchFor — so pcbnew and footprint_editor link when the
3D viewer is off (KICAD_BUILD_3D_VIEWER_WASM=OFF). The KiCad 10 rebase made
dialog_migrate_3d_models.cpp reference these, which live in the unlinked 3D
library. The browser build has no 3D viewer, so model migration finds no
substitutions. Bumps kicad to the matching CMake wiring (fef0343).
Verified: pcbnew + footprint_editor compile and link clean (eeschema/symbol_editor
unaffected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The crosshair toolbar group-cycle (ACTION_TOOLBAR::onToolEvent) was
expected-to-fail because the kicad submodule was a pre-10.0 (9.99.0)
snapshot that lacked it. The KiCad 10.0.4 rebase on this branch restores
the behavior, so the test now passes and Playwright flagged it as
"Expected to fail, but passed". Remove the test.fail annotation as the
test's own comment instructed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KiCad 10.0.4 added two trailing optional params (std::optional<VECTOR2I>
aMousePos, wxString* aActiveUrl) to the out-of-line FONT::Draw declaration
in font/font.h. The hand-written stub in the GAL WebGL test app still
declared the old 6-arg form, so it matched no declaration and failed the
CI "Build GAL WebGL test app" step. Update the stub signature to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes two coupled bugs: (1) a tag deployed the demo by snapshotting the STALE registry with no build/publish, so v0.1.1 shipped v0.1.0-era wasm; (2) publish-wasm built -O1/3D-off while ci-ubicloud built -O1/3D-on — two diverging recipes.
wasm-build.yml (reusable workflow_call): THE single build+test recipe, parameterized by opt_level / build_3d_viewer / run_tests / no_cache / upload_output. Two-tier output cache around build.sh's new --compile-only/--postprocess-only split: a BASE cache keyed opt-INDEPENDENTLY (compile output + sysroot headers, shared across -O1/-O2) and a FINAL cache keyed opt-SPECIFICALLY. final-hit ⇒ skip all; final-miss+base-hit ⇒ restore base, run only the asyncify/-O tail; full-miss ⇒ compile then postprocess. Both keys now include the 3D flag, closing the cache-poisoning divergence.
ci-ubicloud.yml: thin caller (main/PR/dispatch) → wasm-build at -O1. release.yml (tag v*): meta → build (wasm-build at -O2 + e2e gate, uploads output, reuses main's base cache so only the -O2 tail rebuilds) → publish-wasm (content-addressed push + manifest-<tag>.json) → deploy-demo (build-demo pinned to that manifest + libs/kicad/$LIB_TAG → Pages). So the shipped build is exactly the tested build, and the demo can never point at stale wasm.
deploy-demo.yml: demoted to manual dispatch-only re-deploy (keeps the --from-registry snapshot for re-shipping an already-published tag). publish-wasm.yml: deleted (folded into release.yml + wasm-build.yml).
Validated: actionlint clean (bar the known ubicloud custom-label note), YAML + needs-graph + reusable-path checks pass. NOT runtime-tested — the build can't run locally; needs a CI smoke (a PR exercises ci-ubicloud→wasm-build; a throwaway tag exercises release.yml) before relying on it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The container compile produces an OPT-INDEPENDENT base wasm; the only opt-dependent work is the final 'wasm-opt -O$LEVEL' shrink in apply-asyncify (host post-process). Splitting the build at that boundary lets CI cache the expensive compile ONCE (shared across -O1/-O2) and re-run only the asyncify/-O tail per opt level.
--compile-only: in-container compile → output/ base wasm (no post-process). --postprocess-only: pure host dyncall+finalize+asyncify+wasm-opt -O on the existing base (no container; get-wasm-opt self-provisions Binaryen). Default 'both' is unchanged. Phase flags are extracted before ARGS so they are not forwarded to inner build-<app>.sh. bash -n + shellcheck -S error clean; extraction unit-checked incl. the empty-after-filter case.
Enables the two-tier CI cache (opt-independent base + opt-specific final) so an -O2 demo release reuses main's compiled base and only runs asyncify+O2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- kicad → a2c634f2: drop redundant wasm guards, restore upstream
(modified upstream files 95→86, churn 3180→3104)
- kicad-diff-stats.sh: fix fork-point detection (add missing commit authors
+ an @emergence-engineering.com domain net) so it reports the real
divergence instead of 0
- build-kicad-target.sh: drop dead -DKICAD_PCM and -DBUILD_GITHUB_PLUGIN
flags (not options in this KiCad version); keep -DKICAD_SPICE=OFF, which is
read by wasm/cmake/Findngspice.cmake
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
publish-libs.ts: --clone <dir> shallow-clones kicad-symbols + kicad-footprints at --lib-tag (full, all libs) and uses them as sources, so CI publishes the whole set with one command.
publish-libs.yml: decoupled workflow_dispatch (mirrors publish-wasm.yml) — lib_tag input (default 10.0.3) + force; clones + publishes to R2, skip-if-exists keyed by the tag. Dependency-free import chain ⇒ no workspace install (just node + npx tsx + git + wrangler).
deploy-demo.yml: LIB_TAG=10.0.3 → build-demo --lib-tag, so the demo serves the full library set from libs/kicad/<LIB_TAG>. Validated locally over .cache: publish then skip-if-exists; both workflows valid YAML.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract-libs.ts gains extractAllLibs(): enumerate EVERY <Lib>.kicad_symdir / <Lib>.pretty under the source dirs into in-memory self-contained bodies (reuses the curated path's parse + extends resolution). At KiCad 10.0.x symbols already ship the one-symbol-per-file .kicad_symdir layout, so no split needed.
publish-libs.ts (tsx): per lib, build a SyncManifest (sha256 per item) + encodeBundle, write libs/kicad/<libTag>/<lib>/{manifest,bundle} (immutable) + a top manifest.json listing every lib. Keyed by upstream KiCad lib tag, skip-if-exists (HEAD the top manifest; --force overrides) so it is decoupled from the app deploy. Local + r2 drivers via cdn-store.
Validated over web/backend/.cache: 8 libs / 2154 items; Device bundle decodes to 537 bodies, its /manifest matches the bundle, and symbol/R body hash matches its manifest entry (warm-sync diff stays empty). Same wire format cdn-source.test reads, so publish and client are pinned to one format.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backendless, read-only LibsSource that serves the full default symbol+footprint set from versioned immutable CDN snapshots. Each lib is an r2-idb-sync static origin opened as a one-layer SyncStack (1 bundle cold, 0 fetches warm, IDB-cached, offline) — the layer descriptor is built locally from lib id + tag, no resolve endpoint in the loop.
Wired via VITE_LIBS_SOURCE=cdn + VITE_LIBS_MANIFEST_URL; build-demo gains --lib-tag (points the demo at libs/kicad/<tag>/manifest.json; omitted keeps offline static). Unit-tested against a fake CDN built with the real encodeBundle/sha256Hex codecs, pinning the publish format.
Next: publish-libs.mjs (C1.2) emits this format over the full set; CI keyed by lib tag (C1.3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the closed app: create a templated file then open it, so you always edit a real file that saves back.
- lib/new-file.ts: minimal valid KiCad templates (pcbnew/eeschema/pl_editor, from the e2e fixtures) + document-tool helpers.
- NewFileDialog: in-project mode (file name) and home mode (pick/create a browser-local project + file name); writes via the active source uploadFileBytes (local IDB or remote-rw backend), then opens by path.
- HomePage: a document tool from the grid (local store on) opens the dialog instead of booting a throwaway blank doc.
- ProjectView: project-centric New-file row (writable projects) + Save-a-copy-to-browser fork for read-only gallery projects (copies into editable IDB), alongside files/export.
Also replaces a stray NUL byte that had been written into idb-project-store.ts in place of a space (made the file parse as binary) with an explicit unicode escape for the key separator (NUL, sorts below every slug char so project key ranges cannot collide) and the range upper bound. Runtime behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Home page feedback: merge "Your projects" (local) and the gallery/backend list
into one "Projects" section where each ROW carries its own source chip (instead
of a per-section header chip). Sections are now Open → Projects → Tools →
Libraries. Local rows keep open/export/rename/delete; remote rows just open.
LocalProjectsSection → ProjectsSection.
Source chips were too pale on the editor canvas — switch from translucent pastel
to solid fills with white text + an inset ring, so they read on any backdrop
(home, dark boot screen, light editor canvas).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A small fixed bottom-right overlay (VersionBadge) shown app-wide — on the home
page AND inside the loaded editor — surfacing this build's release tag and a
link to the source. The tag links to the exact commit when known (the pcbjam
commit pins the kicad + wxwidgets submodule revisions → our GPLv3
corresponding-source pointer, mirroring site Footer's BUILD_SHA), else the tag's
release page, else the repo root.
build-demo.mjs injects VITE_APP_TAG / VITE_GIT_SHA / VITE_REPO_URL (new --repo
flag, defaults to github.com/emergence-engineering/pcbjam).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an output-cache layer to ci-ubicloud.yml so a build whose inputs are
unchanged (typical when only tests/ or web/ change) skips the in-Docker
compile + the 1-2h host asyncify/wasm-opt chain — the bulk of the ~1h52m
run. On a hit, output/ and the GAL sysroot headers are restored from cache
and the deps restore/seed, build.sh, and sysroot export are all gated on a
miss, so Docker is never started.
Key: kwasm-<os>-bin<ver><opt>-k<kicad-sha>-wx<wx-sha>-sc<hash>-e<epoch>.
The "sc" hash (scripts/deploy/wasm-cache-hash.mjs) folds in just the
build-logic files that shape the wasm bytes (host post-processing,
per-tool compile recipes, scripts/deps, docker/Dockerfile+build.sh) plus
itself; it is content-based, order-independent, and identical on macOS/CI.
*.wasm.debug.wasm (5+ GB, unused by tests) is excluded -> ~0.5 GB entry on
Ubicloud's transparent 30 GB/repo/week cache.
Invalidation:
- bump .ci-cache-epoch in a commit for durable busts (inputs the sc-hash
can't see: base-image/apt drift, a bad cache);
- [no-cache] or [rebuild-wasm] in the commit message / PR title, or
workflow_dispatch no_cache=true, for a one-off rebuild (split
restore/save so the bypass still refreshes the entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GITHUB_REF_NAME is 'main' on workflow_dispatch, so the old ${REF:-input} order
labeled every dispatched deploy 'main'. Prefer github.event.inputs.tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Project not found errors when pcbjam-demo doesn't exist yet; create it (with the
production branch matching --branch) before deploying so the pipeline is
self-contained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo root has no package.json, so pnpm/action-setup can't infer the version
(it lives in web/package.json). Pin it explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The publish scripts shelled bare `wrangler` (ENOENT without a global install)
and the existence probes (registry.json / per-tool meta.json) printed wrangler's
'key does not exist' on a fresh bucket. Default to `npx wrangler@4` and capture
stderr on probes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds KiCad's 3D viewer for the browser (opt in with BUILD_3D_VIEWER=ON) and
wires up the legacy-GL emulation it needs.
- build-kicad-target.sh / docker/build.sh: BUILD_3D_VIEWER →
-DKICAD_BUILD_3D_VIEWER_WASM=ON + -sLEGACY_GL_EMULATION + the GL js-library.
- wasm/shims/gl_immediate_shim.js: display-list emulation (record/replay board
layers), fixed-function + GLU stubs, throw-guards for unsupported pnames, and
per-context GLImmediate init for the viewer's SECOND WebGL context.
- tests/kicad/3d-viewer.spec.ts + utils/pcbnew-ready.ts: open View → 3D Viewer.
- bumps the kicad submodule (EMSCRIPTEN-guarded GL changes).
WIP: the viewer window + UI render and geometry draws (glError=0x0), but the
board is not yet visibly rendered — GLImmediate's FFP shader program isn't
linked on the viewer's 2nd WebGL context. Full status + next steps in
features/feat/add-3d-view/notes.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The demo showed "Live demo unavailable in this browser" on fully capable
browsers (e.g. Chrome) whenever a page load wasn't cross-origin isolated —
a soft SPA navigation, or a visit cached from before the COOP/COEP headers
went live. The old reload guard used a sticky sessionStorage flag that, once
set during a non-isolated load, never cleared, so it skipped the recovery
reload and showed a misleading "unsupported browser" message.
- Reload guard now uses a timestamp, not a sticky flag: a stale flag from an
earlier visit no longer permanently strands the demo (reload loop still
prevented within a 10s debounce).
- Non-isolated pages now open the standalone viewer (/gerber-demo/, isolated
on its own and always works) in a new tab instead of dead-ending.
- The "needs a recent browser" note only shows when provably incapable
(isolated but missing SharedArrayBuffer/WebGL2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Blog: porting-kicad-graphics-to-webgl post + GAL native/WebGL comparison images and <GalCompare>
- Landing page: embed lazily-loaded Gerber demo section + footer "Live demo" link
- Licensing: /licenses page (GPLv3 + wxWindows/LGPL-v2 breakdown, ahilss credit, corresponding-source offer) and a footer build-SHA link
- Docs: CLAUDE.md + README note to bump the hardcoded footer BUILD_SHA on each release
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Headless node-WASM CLI that converts legacy symbol libraries to .kicad_sym via
SCH_IO_MGR::ConvertLibrary. Adds wasm/cli/sym_convert_main.cpp + sym_convert_pre.js
(in-memory localStorage + wxConfig JS-hook shim), registers sym_convert as a
docker/build.sh app (own kicad-sym_convert tree, real wasm-opt/finalize in-container,
skip host postprocess), and bumps the kicad pointer for the matching target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>