The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
The editor reported nothing when a session died. Evidence lived only in-tab —
an 800-line React array behind a "Show console" button — so diagnosis meant
asking a user to paste a screenshot.
Better Stack's Error Tracking ingests the Sentry wire protocol, so this runs
the stock @sentry/browser against a Better Stack DSN. Sentry.init installs its
own window error/unhandledrejection handlers, so uncaught main-thread errors
and the wasm traps that escape emscripten's DOM event handlers are captured
with no instrumentation at the throw sites. Not their JS tag: it has no
beforeSend or fingerprint hooks, its runtime spawns workers from cross-origin
CDN hosts (this page is COEP: require-corp), and it ships session replay on by
default — which on a CAD canvas records customers' board geometry.
@sentry/browser is imported in exactly one file so the vendor stays swappable,
mirroring how lib/analytics.ts isolates Plausible.
Also replaces the terminal-signature regex with a shared, unit-tested predicate
(wasm/terminal-error.ts) used by BOTH the fatal overlay and the reporter, so
they cannot disagree. The regex was a type check written as a string match and
had three live holes: `RuntimeError` was listed but never appears IN
`.message`; Chrome's bare "unreachable" and "null function" matched nothing
(the v0.1.20 prod log is exactly those); and narrowing "table index is out of
bounds" to `\bindex out of bounds` for Firefox in 197f317 silently stopped
matching Chrome's spelling. Checking the TYPE — every trap in this family is a
WebAssembly.RuntimeError — covers all engines and ends the spelling chase; the
message patterns remain as a fallback for paths that lose the Error object,
such as a worker ErrorEvent crossing the realm boundary with error: null.
197f317's pthread-worker tap, promote() and Firefox findings are kept as-is.
Notes:
- Off unless VITE_ERRORS_DSN is set AND VITE_ALLOW_USER_OVERRIDE !== "1" (dev
servers and every Playwright harness set the latter, and production builds
never do), so a production DSN in a local .env still cannot report. With no
DSN the whole SDK is const-folded out: 1,193,080 vs 1,282,463 bytes of JS.
- browserApiErrors integration removed. It wraps setTimeout/rAF/addEventListener
in try/catch, which is exactly how KiCad-on-Emscripten drives its main loop.
- Console breadcrumbs off (collab/debug.ts's clog fires per Yjs update and would
evict the ring before any crash); dom/fetch/navigation breadcrumbs kept.
- beforeSend redacts token/apiKey/Bearer — collab/provider.ts puts the collab
token in the y-partyserver URL, so a connection-failure string carries a live
credential — and guards the cascade: one wedge produced 8 errors in prod, and
after the first terminal event the rest are dropped into cascade_count.
Verified end to end against the real EU host from a cross-origin-isolated page:
POST /api/<id>/envelope/ -> 200, and 4 terminal throws produce 1 event
(control: 1 throw, same count).
Privacy policy 9, cookie policy 6 and the licenses page are updated: Better
Stack is disclosed as an EU processor, and the licenses page now describes the
browser app's own JS dependencies, which it never did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every repo moved from emergence-engineering/ to PCBJam/, but the published
source pointers still named the old org. GitHub's transfer redirect resolves
them (all six checked, 301 -> 200), so nothing is broken today — but it stops
working the moment anyone creates a repo at an old path, and a GPLv3
corresponding-source pointer is a poor thing to leave depending on a redirect.
CI never passes --repo, so the hardcoded default is what actually ships: the
editor's version badge on editor.pcbjam.com has been linking users to the old
org for their source.
Covers the source pointers (licenses.md, terms.md 12.4, REPO_URL and its doc
comment, the three build-script --repo defaults, the site footer's build-commit
link) and the two "our GitHub" org links. Bumps the pcbjam-shared pointer for
the same fix there.
Deliberately untouched: emergence-engineering.com, the company domain behind
contact@ and the EE credit block — the trailing slash in the substitution keeps
it out. And docs/security-audit-glm/, which describes a finding rather than
linking anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Vercel -> Cloudflare Pages move is done and the Vercel project is
deleted, so the one-shot scripts have no remaining purpose. Nothing in CI
ever called them — deploy-site.yml runs npm ci / test / build / pages
deploy inline — so this removes 10 files and orphans nothing.
Deleted: 00-baseline (refused to run without x-vercel-id, so permanently
unrunnable), 01-preflight (proved Vercel state and API-token scopes),
07-dns-cutover (the phased cutover; in the end the records were attached
through the dashboard, and the rules/apex phases went unused once we chose
APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus
03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy,
02-verify-local and 99-rollback, all either spent or duplicating CI. Their
lib/cf-api.sh went with them: the survivors use wrangler, so the whole
remaining path needs only `wrangler login` and no zone scopes.
What is kept is the part with ongoing value: lib/parity.sh, the assertion
set that caught five real defects during the migration — the live COOP/COEP
bug on the post's canonical URL, the soft-404 Pages would have introduced,
the cross-site form-POST guard Vercel had been providing for free, the
missing immutable header, and HSTS max-age=0. "Does the page return 200"
catches none of those.
08-verify-prod.sh becomes verify.sh, since the numbered sequence it
belonged to no longer exists. It drops the stamp machinery, the dry-run
plumbing and the Vercel-fallback messaging (there is no fallback now:
recovery is promoting a previous Pages deployment), and gains --skip-dns /
--skip-domains so it can be pointed at a single deployment via PROD_BASE
before promoting it.
The README is rewritten around the four invariants that fail SILENTLY —
never widen _headers to /*, keep both URL forms of the Gerber post, never
delete 404.astro, keep the cross-site form-POST guard — each with the
reason, since the reason is the only thing that stops someone simplifying
them back out.
Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs
Vercel's 2 years — on, just shorter), 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
Vercel was doing the apex->www 308 itself (its "redirect to www" project
setting), so nothing about Cloudflare requires a redirect — the behaviour
just disappears with Vercel. Rather than rebuild it with a zone Redirect
Rule plus a proxied placeholder record, attach pcbjam.com as a SECOND
custom domain on pcbjam-site. Both hosts serve the site and the pages
already emit canonical=www, which is what consolidates them for search.
That drops the riskiest artefact in the migration. Redirect Rules are
zone-scoped and run BEFORE Workers/Pages routing, so a `contains` match
instead of `eq` would 308 app./editor./demo./api. to www — breaking the
product API, not just a marketing page. The sibling hosts are also the
reason this was worth avoiding rather than merely guarding.
APEX_MODE (lib/common.sh) selects the topology, defaulting to `serve`.
08-verify-prod.sh now dispatches through assert_apex: in serve mode it
requires the apex to answer 200 with no hop, to not be a stale Vercel
response, to declare canonical=www, and to expose /api/waitlist. The
`redirect` mode and 07's rules/apex phases are kept for the alternative.
08 also checks the attached domains via wrangler rather than the REST API,
so the whole serve-mode path needs only `wrangler login` — no zone scopes
at all.
Comments that explained themselves via the old redirect are corrected:
astro.config.mjs, web/standalone/src/lib/config.ts and
scripts/deploy/build-demo.mjs. The demo keeps posting to www — not because
the apex redirects, but because a CORS preflight cannot follow one, so
aiming at a host that might ever redirect is a latent breakage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
www.pcbjam.com was the last piece of the stack on Vercel. It is now a
Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on
every push to main touching site/** — content must not wait for a
release tag.
The Astro adapter is gone entirely: the build is pure static and the one
dynamic route, /api/waitlist, is a Pages Function. Going adapter-free
(rather than swapping in @astrojs/cloudflare, which has dropped Pages
support and only targets Workers) removes three problems at once — no
Astro/adapter major-version coupling, Footer.astro's build-time execSync
keeps working because prerendering stays in Node, and image optimisation
stays plain build-time sharp with no Cloudflare Images binding.
Verified against a real Pages runtime (wrangler pages dev): 21/21 parity
probes pass, versus 19/21 on live Vercel. The scripted runbook is in
deploy/site/ — every mutating step is dry-run by default.
Four behaviour differences were found by measurement and are handled here:
- The blog post's COOP/COEP was already broken in production. vercel.json
scoped the headers to the bare URL, but the page's own canonical is the
trailing-slash form, which served 200 with no isolation headers — so
search arrivals lost SharedArrayBuffer and the embedded Gerber viewer
degraded. public/_headers covers both forms.
- Pages answers unknown URLs with the homepage at HTTP 200 when the
output has no 404.html — a soft-404 that invites indexing junk URLs as
the homepage. Hence src/pages/404.astro.
- Vercel's edge refused cross-site form POSTs ("Cross-site POST form
submissions are forbidden"); Pages does not, and a cross-site <form>
submit needs no CORS permission to be sent, so the allowlist cannot
stop it. The Function reproduces the guard; JSON posts stay exempt as
that is demo.pcbjam.com's allowlisted path.
- Cache-Control: immutable on /_astro/* came from the Vercel adapter's
generated route config, so it is now an explicit _headers rule.
Secrets move to `wrangler pages secret put --project-name pcbjam-site`
(RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL);
WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code.
Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars
does not cover a nested git repo.
privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those
mentions are removed and the existing Cloudflare entry widened to cover
website hosting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
- gerber-demo/boot.js honors the base/cdn/tag script-source overrides only on a
loopback hostname, so a shipped page ignores them.
- /api/waitlist gains a best-effort per-IP/per-address rate limit and masks the
submitter address in the no-key log line.
Adds vitest to the site: test/boot-overrides.test.ts (happy-dom) and
test/waitlist.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All pcbjam properties now load the same new-format Plausible script
(https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js — one dashboard,
segment by hostname) instead of per-domain data-domain sites:
- site: replace Vercel Analytics with the pa- snippet in BaseLayout
(is:inline, prod-only, crossorigin="anonymous" for the COEP
require-corp routes in vercel.json); drop @vercel/analytics dep.
Legal pages (cookies/privacy) rewritten Vercel -> Plausible.
- standalone: initAnalytics() rewritten for the new script format
(queue shim + plausible.init(), async, no data-domain). The gate is
now VITE_PLAUSIBLE_SRC (script URL, deploy-time opt-in) — unset means
no tracking, so dev checkouts and third-party GPL builds stay clean.
VITE_PLAUSIBLE_DOMAIN is gone.
- deploy: build-demo/build-editor --plausible now takes the script URL;
release.yml/deploy-demo.yml hardcode the (public) pa- URL, so the
repo Actions variable PLAUSIBLE_DOMAIN is no longer used.
Verified: standalone tsc + vite build (pa- URL present with env, absent
without, crossOrigin kept); astro build has the snippet on all layout
pages and no vercel/insights references; astro dev stays untracked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commenter role in the viewers/commenters FAQ (reader = view, commenter =
+add comments, editor = +edit); real-time collab listed on Free only, Pro/
Enterprise inherit via 'Everything from ...'; Team states seats are Pro
seats scoped to that team's projects only, plus a matching FAQ entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RQcUBzy8mpYXkup9eNY1z
Free/Pro live, Team/Enterprise as coming-soon cards; monthly/yearly price
toggle; CTAs open a coming-soon dialog pointing at the waitlist; Pricing
link in header nav + footer; header chip nowrap + earlier hamburger
breakpoint; CheckItem gains an optional dimmed note suffix.
Copy source of truth: docs/features/pricing/copy.md (root repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RQcUBzy8mpYXkup9eNY1z
Now that demo.pcbjam.com is live, the landing page leads with it instead
of "coming soon":
- Hero: "Open the live demo" is the primary CTA; the coming-soon panel is
replaced by a DemoLaunchCard (screenshot + play overlay, new tab — the
editor needs COOP/COEP so it can never be iframed here); waitlist demoted
to a ghost button under a "Want early access to multiplayer?" lead-in.
- Removed the Gerber demo and positioning-map sections, then restored the
positioning map per review — recolored for light theme and without the
multiplayer badge/legend ("Everyone picks one. We do both.").
- Light theme: flipped the :root ink tokens, darkened accent/signal for
contrast on white; fixed two dark-assumption bugs it exposed (header CTA
text losing to .nav>a specificity, DemoVideo play scrim derived from --bg).
- Copy: positioned around what's live (real KiCad in a tab) with
multiplayer explicitly "coming — join the waitlist"; cut untrue claims
(GitHub import, self-host, offline PWA -> roadmap); cut the jam metaphor
outside the product name; TrustBar fake-stat grid -> plain claims strip;
footer tagline + fallback meta description aligned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
Trim the waitlist microcopy on the standalone demo and the marketing site's
CTA/hero forms to just the value prop, removing the "no spam, unsubscribe
anytime" reassurance line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
Demo (web/standalone): retitle "KiCad Web" -> "PCBJam - KiCad demo", add
description + OG/Twitter tags (absolute URLs pinned to demo.pcbjam.com),
ship favicons (page had none), add the og-demo.png share card, and show an
"Early-access alpha" badge next to the home-page heading.
Site: add the missing og/og-home.png (BaseLayout referenced it but the
asset never existed -> og:image 404ed in prod) and set astro `site` so
canonical/OG URLs resolve absolutely (prerendered pages shipped
canonical=http://localhost:4321/ to production).
Cards are 1200x630, generated from the favicon.svg logo mark on a pure
white background.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refer to demo.pcbjam.com from the landing: an accent "Live demo" link in the
header nav, an "Open the live editor" hero link, a note in the gerber section,
and a "Try the live demo now" line in the final CTA.
Rework the embedded gerber viewer (public/gerber-demo/boot.js) to source the
WASM from the deploy pipeline's versioned CDN instead of the hand-synced
assets.pcbjam.com bucket: resolve gerbview's content-addressed folder at runtime
from the release manifest (manifest-latest -> tag -> manifest-<tag> -> gerbview),
load all assets (glue + wasm + images.tar.gz) from cdn.pcbjam.com, and load the
cross-origin pthread worker via a same-origin blob importScripts shim (mirrors
web/standalone/src/wasm/boot.ts). Bump the config-seed KICAD_VERSION_DIR
9.99 -> 10.0 to match the deployed build. Drop the now-obsolete committed glue
mirror and the old assets.pcbjam.com sync/r2-deploy scripts.
Collapse the header nav to the hamburger below 1025px — the added demo link no
longer fits the 1024px-capped bar on a single row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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
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>
Embed KiCad's gerbview WASM in the graphics-to-WebGL blog post as a
click-to-load demo that opens the tiny_tapeout board entirely in-browser.
- GerberDemo.astro: lazy iframe embed + cross-origin-isolation reload guard
+ WebGL2/SharedArrayBuffer feature-detect with a poster fallback
- public/gerber-demo/{index.html,boot.js}: self-contained gerbview boot
harness (config seed, MEMFS board preload, argv auto-open)
- board/ tiny_tapeout layers + poster.png committed; wasm/ glue JS committed
(served same-origin — pthread worker can't be cross-origin), while
gerbview.wasm + kicad-resources.bin are git-ignored and served from
Cloudflare R2 (assets.pcbjam.com)
- COOP/COEP require-corp scoped to the post + /gerber-demo routes
(dev: astro.config.mjs + middleware.ts; prod: vercel.json)
- post converted to MDX (@astrojs/mdx) so it can embed the component
- scripts/: sync-demo-wasm.sh, r2-deploy.sh, r2-cors.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Chrome also fires an implicit request for /favicon.ico; with only an SVG
link declared and no .ico at the site root, that 404 left the tab without
an icon. Add a square multi-res .ico (48/32/16) and a 180x180
apple-touch-icon — both derived from the jar art already embedded in
favicon.svg — and declare the full icon set (ico + svg + apple-touch) in
BaseLayout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the /privacy placeholder with the full GDPR-first policy (dated
10 Jun 2026). Beyond the reviewed draft: cover the waitlist end-to-end
(data row, consent basis, marketing section, retention), add a staged-
rollout clause so future-product processing is described honestly, make
the CCPA/GPC commitment conditional (no voluntary promise while nothing
is sold/shared), and fix the CCPA look-back wording.
Point of collection follows suit: the waitlist form microcopy links the
policy, and the confirmation email now offers reply-to-remove, promises
an unsubscribe link in every update, and links /privacy.
- replace the placeholder terms page with the real ToS (EU/Hungarian
consumer law, Paddle Merchant-of-Record billing, GPLv3 corresponding-
source offer), dated 10 June 2026
- serve legal pages from a markdown content collection rendered by
src/pages/[legal].astro; drop the per-page placeholder .astro files
(privacy/cookies keep placeholder content, now as markdown)
- add prose/table styles for legal pages
- footer link text: "Terms & Conditions" -> "Terms of Service"
Canonical working copy + clause-by-clause legal sourcing live in
features/ (untracked research).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Install @vercel/analytics and render <Analytics /> from BaseLayout so it loads on every page. Auto-activates in production on Vercel (no-op locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CSS-only hamburger (checkbox toggle) only reset on astro:page-load, which fires for real navigations (Blog) but not for same-page anchor links (How it works / Open & yours / Join the waitlist) that scroll without navigating — so the menu stayed open after tapping them. Add a one-time delegated click handler on the nav that unchecks the toggle on any link tap. Items stay identical to desktop; scrolling unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move secrets to Astro's typed astro:env/server (schema in astro.config.mjs);
removes process.env usage and the 3 TS2580 "Cannot find name 'process'" errors
without adding @types/node.
- Replace deprecated contacts.create({ audienceId }) with segments: [{ id }]
(env var RESEND_AUDIENCE_ID -> RESEND_SEGMENT_ID).
- Check the { error } return on both Resend calls: contact add is best-effort
(non-fatal); a failed confirmation send now returns 502 instead of a false 200.
- Type the waitlist fetch result in WaitlistForm.astro.
- Add @astrojs/check + typescript devDeps and an `npm run check` script.
Verified: npm run check (0 errors), npm run build, secret not inlined into the
build output, and a runtime smoke test of the validation/honeypot/GET paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use PCBJam's actual address (hello@pcbjam.com) as the waitlist confirmation
sender, in the API route default and .env.example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- WaitlistForm: reset the input's flex when the form stacks — the 16rem
flex-basis was applying to the height in column layout (256px-tall input).
- DemoVideo: add a comingSoon mode that shows a big "Demo coming soon" panel
instead of the internal PRODUCE placeholder; enable it in the hero and the
multiplayer section.
- Blog: retitle the post to "Porting KiCad's Graphics to WebGL with Claude"
(clean slug), set body/description to "Coming soon.", and drop the
placeholder text from the blog index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the ai.svg glyph (and its import/flex styling) from the "AI when you want
it" section per founder review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Founder review pass on the landing page, plus commit the landing-page
research/spec docs under features/lp.
Branding & assets:
- New PCBJam mark: convert the jam-jar image to favicon.svg (trimmed, rounded
tile) and use it in the browser tab, header, and footer.
- Footer "Built by": flower mark + "Emergence Engineering" in white Oswald (the
blog's title font, self-hosted via @fontsource) so it's legible on dark.
- Produce the positioning quadrant graphic (public/graphics) and drop the placeholder.
Copy / accuracy:
- Remove header GitHub link; point "Built by EE" + footer "About" to
emergence-engineering.com; Built-by CTA -> mailto:contact@emergence-engineering.com.
- Replace the wrong testimonial with Greg Detre's real quote from EE's site.
- Drop blog icons from Multiplayer/Pillars/How-it-works; add "Coming soon" to
live-cursors & pin-comment; drop the hard-coded Yjs mention.
- Rework monetization to free+local / paid cloud-sync; remove "no metered AI",
"no per-seat toll", and the Flux price claim; soften the AI section.
- Reframe waitlist copy (incl. confirmation email) to newsletter + early access.
Fixes:
- Offset only #waitlist for the sticky header (scroll-margin-top); section
anchors keep their original behavior.
Docs:
- Commit features/lp (copy spec, brand guide, competitive research) via force-add
over the /features/ gitignore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone Astro 6 site in /site (decoupled from the /web app monorepo):
landing page, /blog with one post (Content Layer), and terms/privacy/cookie
legal pages. Pure Astro, server-rendered to static HTML.
- Static by default via @astrojs/vercel adapter; any route can opt into SSR
with `export const prerender = false` (deploys as a Vercel Function).
- SPA-style navigation with <ClientRouter />, transition:persist on the header
(no icon flash), and viewport prefetch — ~6 KB gzip JS, no React.
- Deploy on Vercel by setting Root Directory to 'site' (no vercel.json needed).
See site/README.md for dev and deploy instructions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>