Commit graph pcbjam/web/standalone/src/components/WasmTool.tsx
Author SHA1 Message Date
Gergő Törcsvári
84a40d4492
diag(asyncify): write-time instrumentation + local warm-load repro findings
The prod differential ladder finished: staged byte VOLUME on a warm load is
the only trigger left (V1a siblings-without-lib-tables dies, V1b +120 files
survives, V1c sibling KiCad files renamed byte-for-byte dies, V1d Leonardo +
123MB of inert markdown dies on loads 3-4; 14MB never dies). 3D models,
collab/ydoc/presence, lib tables, sibling KiCad handling and file count are
all exonerated — volume only loads the dice on the underlying race.

That made the crash reproducible locally for the first time in six campaigns:
a persistent browser profile + a 110MB project fails every warm load with the
exact prod signature. Iteration is now ~12 minutes instead of a release cycle.

Shim: every fiber switch now records the departing side's remaining asyncify
buffer and its recorded rewind entry (rem=/rf=), which is what identified the
unrewindable capture and disproved buffer overflow. The deferral family is
closed for good — a microtask-deferred retry on a clean empty stack died
identically to the nested rewind, because the suspension is broken at write
time, not by nesting.

Shell: log the origin stack when wx reports the top window destroyed. That
notification fires from ~wxTopLevelWindowWasm for ANY top-level window, so a
transient frame dying mid-load navigates the user out of the editor — a real
bug in its own right, found while chasing the empty flight-recorder dumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-03 11:05:10 +02:00
Gergő Törcsvári
11da3fce19
fix(editor): ?collab=0 is a full kill-switch — doc room + materialization included
The 8/2 crash-hunt bisection attempt with ?collab=0 was silently invalid: the
flag only gated the attach, while the boot fan-out joined the doc room and
materialized the target file from the ydoc regardless (the "collab=0" prod
log shows both, plus an attach). The flag now also skips the doc-room join —
the file falls back to the plain fetch path — making it a real lever for the
warm-siblings crash bisection (ydoc-vs-sexpr file source, the next suspect
after sibling restage and collab attach were exonerated) and an honest
user-facing escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-02 13:09:52 +02:00
Gergő Törcsvári
5c7f8a2e85
fix(editor): stagger the sibling restage out of the settle window
The differential-repro ladder (2026-08-02, five prod runs + local counter
measurements) narrowed the crash trigger empirically: warm loads of
sibling-heavy projects die at settle (V1/V4 fail 2/2 warm; V2/V3 without
siblings never fail, warm or cold), while the flight-recorder counters show
the settle-time collision windows themselves are universal (fcsTotal=72,
rootHotTotal=3 on V1 AND V3, every load, cold and warm — so the windows are
the shared fan-out, not sibling-made). The sibling restage's room connects +
restage fetches are the only sibling-specific traffic contending with those
windows, and warm IDB compresses it into exactly that moment.

Nothing in the restage is needed for first paint — the boot snapshot staged
every sibling seconds earlier — so it now starts on requestIdleCallback
(5s timeout; setTimeout(3s) fallback), well clear of the settle storm.
Unmount-safe via disposedRef (armed per mount, checked in the deferred
starter and on handle resolution).

Validation is empirical by design: the counters won't move (windows are not
sibling-made); the test is warm V1/V4 prod loads no longer dying. If they
still die, the sibling lever is exonerated too and the remaining suspects
narrow to the ydoc-materialization path差 (second-load file source) — the
next probe either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-02 12:13:21 +02:00
Gergő Törcsvári
875195f721
feat(editor): console copy button + collapsible DOM-floor console + one blue family
The first prod blue screen (finally!) also surfaced the console UX gaps: the
in-app log had to be hand-copied (and arrived truncated — the flight-recorder
dump missing), and the DOM-floor screen's log was a fixed block.

- React console: a copy button beside the toggle (writes the full log to the
  clipboard, confirms in the log); stays collapsible on a fatal as in normal
  editing.
- DOM-floor screen: its console mirrors the editor one — toggle bar
  (▾/▸ console) + copy, collapsible — and its content is ring + trace dump.
- One blue family: both fatal screens now use the boot overlay's #1a1a2e, so
  every full-screen state (boot, consent, fatal, floor) is coherent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:50:01 +02:00
Gergő Törcsvári
eff5befd5d
fix(editor): DOM-level blue-screen floor — React can no longer white-screen a crash
v0.1.22's WasmErrorBoundary was still not enough: a commit-phase throw in
WasmTool's OWN effects unmounts the root, and no boundary below it helps.
fatal-screen.ts is the floor: plain-DOM blue screen with its own mirrored
log ring (append feeds recordFatalLog), installed at module import in
main.tsx — before and independent of React. It cooperates with the React
overlay: hidden while [data-testid="fatal-overlay"] exists, takes over via a
1Hz ensure-loop the moment it disappears. Fatal promotions also append the
asyncify flight-recorder dump so whichever screen survives carries the
targeting data.

fatal-overlay.spec.ts now also rips out the React root after the fatal and
asserts the DOM floor takes over with the mirrored [fatal] log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:07:09 +02:00
Gergő Törcsvári
92e02a54f9
fix(editor): the white screen was React unmounting its own crash reporter
All three prod crash cascades (v0.1.19–21) ended white for the same reason:
the final trap lands inside a child's EFFECT (an embind call reached through
a react-query subscription), React unmounts the entire root, and the fatal
overlay + console panel die with the tree they were built to survive.

- WasmErrorBoundary: crash-capable children live inside it; the fatal screen
  and the console panel live OUTSIDE and keep rendering. WasmTool state
  (logs included) survives a descendant render/effect throw.
- The fatal screen is now an actual blue screen (:( + solid blue), and every
  promotion path (window error, unhandled rejection, worker error, boundary)
  auto-opens the console — the log is the only account of what was loading.
- tests/web/fatal-overlay.spec.ts pins the contract: a terminal uncaught
  error after boot ⇒ visible fatal overlay + open console with the [fatal]
  record. Green locally (23.9s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:10 +02:00
Gergő Törcsvári
197f317bfb
fix(editor): fatal overlay matches Firefox trap messages + taps pthread-worker errors
The v0.1.19 prod crash (console-export-2026-7-31_17-49-20.log) opened with
Firefox's bare "index out of bounds" — no "RuntimeError" prefix, no "table" —
which the terminal-signature regex only knew in Chrome's spelling, so the
overlay this feature exists for never promoted on the very trap it was built
against. Match the bare form (+ "null function or function signature", the
other Firefox spelling in this family).

Also wrap the Worker constructor attach-only: a pthread worker's uncaught
error fires an ErrorEvent on the Worker OBJECT, never on window, so worker
crashes (raytracer pool etc.) were invisible to both listeners. The editor's
main()/wx run on the page thread (no PROXY_TO_PTHREAD on the link line) — the
tap is defense-in-depth, not the primary fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 20:33:55 +02:00
Gergő Törcsvári
9ea8b263cc
perf(libs): realtime only for libs the open document references
Under realtime "shared-only" a board/schematic session holds no socket per
org lib — which silently broke the lib-update toast (a peer editing a
PLACED symbol never reached the open session live). Complete the design
with the deferred-realtime upgrade: after open, scan the staged target
document for lib-table nicknames (lib_id / footprint / lib_symbols tokens)
and promote exactly those libs' stacks to realtime via the new
LibsSource.enableRealtime. One socket per lib the document actually uses
(typically 0-5) instead of one per lib in scope (60+); every other lib
still catches up on the next load via the descriptor digests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4EzyhjhDzLdNZsFAYjz7X
2026-07-31 15:43:11 +02:00
Gergő Törcsvári
b5ed68ef87
perf(collab): scope board-load connections to what's actually open
Presence-scoped sibling restage: every tab announces the document it is
actively editing in the project presence room (PresenceState.sheetPath,
re-published on eeschema sheet navigation), and a pcbnew session connects a
sibling schematic's board-room only while a peer announces it open — instead
of eagerly holding one socket per sheet for the whole session. Works because
/files/ materializes from the ydoc, so the boot MEMFS snapshot is room-fresh;
a sheet can only drift while someone is editing it, and that someone is in
the roster. On leave the watch lingers 30s, flushes the pending restage, and
closes; without a presence room the eager mode remains as fallback.

A solo board session now holds 3 sockets (doc, presence, lib-mirror mux) —
down from 92 on an 8-project repo before this series.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4EzyhjhDzLdNZsFAYjz7X
2026-07-31 15:00:51 +02:00
Gergő Törcsvári
7ccedbf973
fix(editor): a browser refresh reloads the editor instead of bouncing away
Refreshing ANY editor URL navigated to the management app's project overview
instead of reloading. Not related to spaces in paths — verified path-independent
by reproducing it on a fileless `-/pcbnew` route with no file path at all.

Mechanism, from probing the actual ordering rather than reading the code: the wx
port registers its unload handler via emscripten_set_beforeunload_callback,
which lands in the CAPTURE phase and so runs before any bubble-phase listener
regardless of registration order. It closes the top frame -> fires
wxAppTopWindowClosed -> the quit dispatcher -> location.assign(exitUrl), and
that navigation overrode the in-flight reload (the reload itself failed with
NS_ERROR_FAILURE).

The quit hook already latched off for our OWN navigations
(markDeliberateNavigation) and on pagehide — which the existing comment notes
fires too late, at commit time. Nothing covered a browser-INITIATED unload:
reload, Back, closing the tab. This adds that latch, in the capture phase so it
precedes emscripten's handler; a bubble-phase listener registered at module
scope was NOT enough, which the probe showed directly.

Tradeoff, documented at the call site: if a beforeunload prompt appears and the
user stays, the latch remains set and a later File->Quit won't self-navigate.
That is strictly better than a page that cannot be refreshed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-31 13:19:10 +02:00
Gergő Törcsvári
bf0e195ddc
chore(editor): load timeline with heap tracking, dumped on any fatal
Every production report of the board-load crash so far has been a console dump
with no timing and no state, which is why five theories died slowly and none
reproduced locally. This records the few facts that would actually discriminate
between the ones still standing, and nothing else.

Marks: fs:wait, fs:ready, stage:done, open:start, open:settled, presync:settled,
ui:ready — on one monotonic clock, each carrying the wasm heap size, with growth
called out explicitly.

Why these:
- The open window is where the crash lives: OpenProjectFiles runs the footprint
  library preload INLINE on the main thread in the WASM build. Bracketing it
  lets a crash be placed inside or outside that window instead of inferred from
  log order.
- ui:ready is first paint. The symbolized trap needs !m_gal->IsInitialized(),
  so a report containing this mark rules that mechanism out and one stopping
  before it does not.
- Heap size at every mark, because growing wasm memory detaches JS-side views,
  and a stale view writing into a detached buffer is one of the few mechanisms
  that yields a bad function-table index much later. Already earning its keep:
  a local Leonardo load grows 256MB -> 443MB during staging, immediately before
  the open. Locally that is survivable; production should not be guessed at.

dump() prints the whole timeline and is wired into all three fatal paths (boot
catch, window error, unhandled rejection), so one paste from a user carries the
full load shape. Bounded at 200 entries; unit-tested including eviction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-31 11:47:14 +02:00
Gergő Törcsvári
1ab46eacd7
fix(editor): show a fatal overlay instead of a blank page, above the console
The boot overlay only renders while `!ready`, so anything that killed the
runtime AFTER the editor came up — a wasm trap, an abort, a staging fetch
failing late — unmounted the last thing on screen and left a white page with
no explanation.

Adds a `fatal` state rendered INDEPENDENTLY of `ready`, set both from the boot
catch and from window error / unhandledrejection listeners that promote only
genuinely terminal signatures (RuntimeError, abort, table index out of bounds,
indirect call signature, unreachable). Ordinary app errors must not hijack a
working editor, so anything else is ignored.

The console panel moves from z-20 to z-40, above both overlays, and is forced
visible on a fatal even with chrome hidden: when a load fails, that log is the
only account of what was loading when it happened, and it was being covered by
the very overlay reporting the failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-31 10:54:21 +02:00
Gergő Törcsvári
d35cf4f4eb
fix(load): close the dispatch-interlock hole at open + open gerbers from a project route
- kicadOpenFile now holds wxWasmDispatchGuard (open_gate.h). It enters through
  embind, so the interlock read "nothing parked" for the whole load and wx timers
  dispatched into the half-built board — the residual prod "index out of bounds"
  that survived the settle gate.
- new wasm/bindings/gerbview_embind.cpp (the bundle had no embind surface at all):
  kicadOpenFile / kicadOpenFiles / kicadOpenFileBusy. Clicking one gerber opens the
  whole fabrication set in its folder, since a lone layer is not a useful view.
- cross-app presence rejoins in the boot fan-out (network-only; the wasm-bound half
  still waits for the open to settle) — it had been pushed behind the board load.
- tests: gerber-set selection units + a gerbview multi-file open e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-30 19:08:57 +02:00
Gergő Törcsvári
a26ef4ebeb
fix(load): open-settle gate — kicadOpenFileBusy probe + collab entry guards for the parked-open embind trap (indirect call signature mismatch) + deterministic collab-load-fuzz e2e
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
2026-07-30 14:17:48 +02:00
Gergő Törcsvári
bd1fbdce85
fix(editor): boot fan-out — parallel presync/doc-room/files, lib-editor enumerate gate, My Symbols 409 (load-ux 0003)
The 7/29 "presync after open" reorder misdiagnosed the slow board open: the
problem was ordering/serialization (libs awaited before serially-fetched
project files), not bandwidth — the ~155 lib requests are latency-bound. It
also regressed the symbol editor, whose frame eagerly enumerates every lib
through the mutex-serialized bridge crossings: with no warm-up running during
the wasm download, each cold lib fetched one-at-a-time inside its own serial
crossing.

- WasmTool: at consent-OK start in parallel — wasm boot, doc-room connect
  (websocket up BEFORE any file fetch; errors captured and rethrown at the
  await), lib presync (concurrency 8, presyncSettled never rejects), and
  project-file staging (the glue runs FS.staticInit() at script-eval, so
  MEMFS staging always overlapped the download).
- Lib editors (fileless): installLibsProvider enumerateGate parks the whole
  "list" op (a plain name list also cold-fetches the bundle) until the
  presync settles, and the boot overlay waits for it before waitForWxUi —
  downloads run 8-wide in JS, the serialized crossings become IDB read +
  parse. pcbnew/eeschema stay ungated (a silent mid-session park would read
  as a hang). The reentry-guard mutex itself is untouched.
- Overlay: "Project files — n/m" staging line (DriveOptions.onFileProgress),
  and the lib line returns worded "Checking <kind> libraries — n/155" (the
  walk checks every lib but downloads only new or changed ones).
- boot: hasWritableLib accepts lib type "org" (the private platform's rename
  of "user") — boot re-created "My Symbols" on every load and the backend
  409'd.

typecheck clean; 239/239 standalone tests; web e2e tools-open 7/7 +
chrome-toggle 2/2 + comments 1/1 (web-firefox).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N925iFAdnVcMCdvS4pzXER
2026-07-30 10:37:12 +02:00
Gergő Törcsvári
2a47103407
fix(editor): warm libs after the editor opens, and say what we actually download
Two fixes to the same complaint — opening a board took minutes and the dialog
explaining why was misleading.

Ordering: the scope-wide lib presync ran "in PARALLEL with the wasm download",
so ~155 libraries' worth of requests competed with the 26MB+ bundle and the
project's own files — the things the user is actually blocked on. It now
starts after driveProjectIntoTool, warming IndexedDB behind an editor that is
already open. Still fire-and-forget and best-effort (the SyncStack dedups, so
a lib the wasm reaches mid-presync awaits the same in-flight fetch), now
abortable on unmount, and no longer shown on the boot overlay as a counter the
user is waiting on — the background indicator owns it.

Wording: the dialog quoted the compressed size with no hint that it was
compressed ("~35 MB" for a bundle whose progress bar then counts ~150 MB raw),
and quoted only the count of libs whose BODIES need downloading — while the
progress bar went to 155, because every warm lib still costs a sync-stack
resolve plus a manifest GET to find nothing changed. It now reads "~35 MB
compressed / ~150 MB uncompressed" and "downloads 1 library, checks all 155
for updates". Every existing degradation path is kept: unknown sizes still
say "large", sizesKnown:false degrades to "at least ~X MB", and the
no-warmth-answer case now says "fetched in the background" rather than the
newly-false "downloaded now".

Investigated and NOT changed: the symbol path is not structurally more
deferred than footprints — both enumerateLibrary() implementations are already
no-ops, so neither face does bridge crossings at boot. The perceived
schematic-vs-board difference was this presync timing plus set size.

typecheck clean; 233/233 standalone tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8jo7zz1ZwzYpjJ64UZKN4
2026-07-29 16:02:22 +02:00
Gergő Törcsvári
8ee8db69e6
feat: standalone download-consent gate + truthful loading states (standalone-load-ux 0001/0002)
Cold loads on versioned CDN deploys now show a consent card (editor MB +
symbol/footprint lib figures, downloaded-now vs on-demand) and wait for OK
before any big fetch; warm loads skip it and show truthful stages (loading
from cache / Compiling / Starting KiCad) instead of the first-download line.

- wasm-assets: resolveWasmMeta (bundle/ver/sizes), download-completion marker
  keyed by content-addressed bundle/ver, update wording, auto-download opt-out,
  HEAD size fallback
- boot: manifest raw size as the progress total (fixes the br/gzip
  Content-Length mismatch), marker written after download+instantiate succeed
- cdn-source: syncState() — IDB warmth peek + sizes.json cold sums
- synced-source: syncState() from the backend envelope's sync refs (private
  platform); remote-source passes libSchema.sync through
- publish-wasm: manifest schema 2 with per-bundle sizes (registry-persisted,
  reuse + snapshot modes); publish-libs: sizes.json sibling key + top-up mode
- fixed 4 stale unit tests (bundle mapping, session-identity email)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011CC8aAnUUHcnHy3QCJtUwb
2026-07-29 07:48:25 +02:00
Gergő Törcsvári
3ff53f0a78
standalone: start headless (chrome hidden) on small screens + treat tablets as mobile
Chrome start-hidden default is now startsChromeHidden(): phones (UA-CH),
tablets (coarse primary pointer at any width), and narrow windows (<=900px,
any pointer). isMobileMode() drops its narrow-viewport requirement so
tablets get the full mobile treatment (touch shim, preflight suppression);
touch laptops stay desktop (fine primary pointer). capabilities.ts probe
kept in sync. ?mobile= still overrides both ways.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkUNWj4KWz6WfP8VwRsUNN
2026-07-27 18:38:02 +02:00
Gergő Törcsvári
21a96b0440
comments-ux: figma bubble pins, floating panel, seen/reactions/mentions UI, theme follow (0001 A–E + 0002)
- GAL pin = one closed polygon: round body, squared-off bottom-left corner
  ON the anchor; PIN gains unread (accent ring); tuner knobs; shipped
  defaults r9/ring4/alpha.9. DOM hit/highlight sized+offset from a LIVE
  pin-geometry radius store the tuner feeds.
- Floating comments panel: draggable (shared useDraggablePanel with
  always-onscreen restore; overlay FAB retrofitted), collapsible to header,
  header carries add/show-hide/mark-all; unread badges (rose on mention).
- Reactions (emoji-mart lazy, quick-row) + @-mention autocomplete
  (MentionInput; backend roster with presence/author fallback).
- Theme: ?theme= > storage > OS, no-flash boot, toggles (HomePage + overlay
  View row), boot-seeded pcbjam-dark schematic colors + kicadSetColorTheme /
  kicadSetDarkChrome bridges (canvas + wx chrome live flip), light/dark
  variants across all overlay surfaces.
- e2e: panel/seen/reactions/mentions/theme specs + resize-spec geometry;
  bumps pcbjam-shared (flat-key seen/reactions + listCollaborators) and
  wxwidgets (dark chrome) pointers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLwn1toiNKi1MgxGKnZTes
2026-07-24 13:21:22 +02:00
Gergő Törcsvári
fc2efeff2c
fix(editor): real comment authors + overlay menu redesign
Comments showed the author's SLUG — which doubles as their personal scope
— instead of their name, because WasmTool passed presenceUser().id where
.name already existed.

- denormalize authorName/authorEmail onto each comment message + thread at
  write time, so a comment still reads correctly when its author is
  offline, renamed, or gone. `author` stays the SLUG: colorFor() and the
  "is this mine?" ownership checks compare it. Legacy comments fall back.
- session-identity keeps the email it was already fetching and discarding.

Overlay menu: restyle + reorganise into labelled sections (People /
Document / Comments / View) over one shared row shape, so sections can't
drift apart again.

- comments: four bare icons -> labelled rows (Add comment / Show list with
  count / Hide pins). The nested expand toggle is gone: the section IS the
  group, so it was a collapsible inside a collapsible.
- presence: facepile of initials -> one row per person with an explicit
  Follow/Stop. The separate "Following X" banner is deleted; that state now
  lives on the person's own row, so there is nothing to keep in sync.
- SourceChip gains a `muted` tone for known-dark surfaces; its solid
  variant is untouched for the light project pages that share it.

Fixes a regression the redesign introduced: the taller panel (z-50) covered
the comment popover (z-40), and since a popover can be opened FROM the
menu's thread list, the menu swallowed clicks on the popover it had just
spawned — delete was unreachable. Popover now z-[60].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
2026-07-20 20:59:24 +02:00
Istvan Matejcsok
8a20f69c5e quit nav: File→Quit always exits to the project overview, never history.back()
Quit used to mimic the Back button (history.back() whenever a referrer
existed), but every in-app entry and every tool switch is a hard
location.assign that pushes a history entry — so after schematic ⇄ pcb
switches, one step back is another editor, not the page the user left from.
Quit now navigates to the project overview explicitly (projectPath; "/" for
lib editors), which also covers deep links uniformly.

New VITE_QUIT_ORIGIN ("" ⇒ same-origin) lets the backed editor deployment
exit to the management app's project page instead of the standalone's own;
build-editor.mjs sets it (override with --app-origin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:36:43 +02:00
Gergő Törcsvári
2d89f41060
project-sync: boot .kicad_pro synthesis + creation-time companion + live sibling restage
- defaultKicadPro() template + synthesizeProjectFile() writes a minimal
  <stem>.kicad_pro into MEMFS when a board/schematic opens without one, so KiCad
  runs a real project (self-heals legacy fileless projects).
- NewFileDialog creates the .kicad_pro companion alongside a new board/schematic.
- sibling-restage.ts: pcbnew sessions subscribe to sibling .kicad_sch collab rooms
  as invisible observers and re-stage MEMFS on update (restageFile extracted from
  syncProjectToMemfs), so a sync reads live schematic data, not the boot snapshot.
- tests/kicad/project-sync.spec.ts (2 e2e) + sibling-restage.test.ts (5 unit).
- Bump kicad pointer (SCH-from-PCB sync gate + hidden player + .kicad_pro save hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdqhM7j71XQn96hcHkabDu
2026-07-16 19:05:23 +02:00
Gergő Törcsvári
f4d3c3f146
collab-presence 0010: unified draggable overlay menu
One circular FAB (peer-count badge) replaces the top-right overlay row:
draggable anywhere (pointer capture, 4px click-vs-drag threshold — the
comment-pin pattern), position persisted in localStorage, panel opens
toward screen center and stacks composable sections — roster, source chip,
view-only pill, follow row, comments, chrome toggle. Renders at z-50 above
everything (decided: covers wx dialogs; trivially dismissed via click-away/
Esc/drag). CommentLayer portals its bar + list panel into the menu's
comments slot; pins/popovers/composer/click-catcher stay canvas-anchored.
The FAB is the chrome-hidden survivor. e2e specs updated with an
openOverlayMenu helper (panel is closed by default, click-away closes it).

NOTE: Playwright suites updated but not executed here (no local WASM
artifacts) — verify on CI / next session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
2026-07-16 17:25:56 +02:00
Gergő Törcsvári
250dc3dbae
collab-presence 0009 C: seed live color claims from comment authors
createPresence accepts a lazy seedColors map (the doc's comment authors):
claims prefer your own comment slot, lowestFreeColor counts authors' slots
as used, colorOf answers offline authors from the seed before the hash.
Comments' own fallback chain gains the seed for presence-less binds.
WasmTool wires the bound doc into both presence paths (pcbnew + eeschema
per-sheet rebinds). Bumps pcbjam-shared (commentAuthorColors). +4 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
2026-07-16 17:25:56 +02:00
Gergő Törcsvári
6675c1f0fc
collab-presence 0009 A: real session identity for presence + comments
presenceUser()/userSlug() now resolve the authenticated user: a plain
/api/me fetch (GPL no-link rule — no closed contract import) races the WASM
boot and is awaited before presence/comments bind. `?user=`/`?libowner=`
overrides are gated behind VITE_ALLOW_USER_OVERRIDE=1 (dev script + e2e
harnesses set it; prod builds never do). Anonymous/example-backend sessions
keep the pre-auth slug fallback. Fixes prod's empty presence (everyone was
"local-user", deduped as own tabs) and comment authorship in one move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
2026-07-16 17:25:56 +02:00
Istvan Matejcsok
2652d6fa81 tool-switch: create the missing counterpart file instead of silently no-opping
Tools → "Switch to PCB Editor" in a project with no .kicad_pcb (e.g. created
from a lone schematic) did nothing: the nav hook found no file for the target
tool and returned false. Now a session that can persist (ToolPage passes the
new createFile prop) writes the templated counterpart via
createProjectFileIfMissing — no download fallback, and it re-checks existence
so a collaborator's file is never clobbered — then navigates to it, matching
native KiCad (pcbnew opens a new board at the derived path). Sessions that
can't persist (read-only viewers, scratch/local-folder) keep the logged no-op.

Also latch the quit dispatcher off before every deliberate tool-switch
navigation (markDeliberateNavigation): the wx port's UnloadCallback runs on
BEFOREUNLOAD and closes the top frame the moment the navigation starts, so
the quit hook history.back()'d over the in-flight navigation — the pagehide
latch is too late (it only fires at commit time).

e2e: new tests/web/tool-switch-missing-file.spec.ts reproduces the flow via a
browser-local (IDB) project created from the home page; playwright-web config
gains VITE_LOCAL_PROJECTS=idb and derives STANDALONE_PORT/CORS_ORIGIN from
WEB_APP_URL (runs the suite past a squatted :3048); both vars declared in
web/turbo.json globalEnv (turbo strict-env strips undeclared vars). Includes
the previously-uncommitted tool-switch spec repairs (URL grammar + z-30
boot-overlay wait).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:19:47 +02:00
Gergő Törcsvári
fb2aa2759d
kicad-validity B3+C+S: revert toast, corpus lint, .kicad_mod tier
B3 (editor): kicad-binding observes kdoc_meta.revertNonce (deduped, like
seedNonce) → DOC_REVERTED_EVENT → WasmTool doc-reverted-toast (15 s,
data-testid for the deferred e2e check).
C: tests/tools/corpus-lint.ts (npm run corpus:lint) — kicad_tools --lint
over the fixture corpora raw + shared-codec round-trips
(docToFile(fileToDoc(x))); the E3/wrapInBoardEnvelope-class gate. Skips
when output/kicad_tools.js isn't built; CI step lands when kicad_tools
joins the CI app set. 7 fixtures / 7 round-trips green.
S: --lint + --resave gain the .kicad_mod tier (pcbToolsLintFootprint /
pcbToolsResaveFootprint; writer needs CTL_FOR_LIBRARY or the board flags
strip the (version) header). Verified: qa footprint round-trip lints
clean, resave stamps 20260206, garbage exits 1. Also bumps pcbjam-shared
(mergeYUpdates for the bisect pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
2026-07-14 19:02:13 +02:00
Istvan Matejcsok
985408ce9e fix(editor): File→Quit returns to the management app it was entered from
The quit hook only history.back()ed on a SAME-ORIGIN referrer, so the
primary closed-deploy entry — app.pcbjam.com deep-linking into
editor.pcbjam.com (cross-origin) — fell through to the editor's own
project view instead of returning to the app. Quit is Back-button
semantics (3a2255c): any non-empty referrer with in-tab history now
goes back; deep links / fresh tabs (no referrer) keep the project-page
fallback. Verified live against the closed stack: :3047 → editor →
quit lands back on :3047; a direct editor URL still falls back to the
editor's project overview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:28:49 +02:00
Istvan Matejcsok
bea14c772f feat(storage): thread the owning team's scopeId into collab rooms
The closed backend's storage rework scopes collab rooms/keys by team:
room ids are now <scopeId>:<projectId>:<docPath> (@pcbjam/shared bump).
The editor threads a scopeId prop parallel to projectId — ToolPage takes
it from the project DTO (falling back to "local" for scope-less
backends; Home/LibTool pages pass "local" explicitly) — through WasmTool
into the doc session, sheet-manager warm rooms, and cross-app presence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:13:45 +02:00
Istvan Matejcsok
bb01f5d9e6 feat(read-only-viewer): editor viewer mode + kicadSetReadOnly binding
Anonymous / non-member sessions open PUBLIC projects as read-only viewers.

- kicadSetReadOnly(bool) embind (merged kicad_editor + pcbnew/eeschema/
  pl_editor TUs): sets the PCBJAM_READ_ONLY flag + Prj().SetReadOnly (greys
  the setup dialogs). Polls until the frame exists.
- read-only-mode.ts: resolveReadOnly(access, win) — server `access:"read"` or
  ?readonly=1 (narrow-only; no ?readonly=0). ToolPage threads it in, omits
  saveBytes (MEMFS-only saves).
- WasmTool: chrome force-hidden with a "View only" pill (toggle + Cmd+\
  disabled), presence/cross-app/comments/drift skipped, save-driven room
  writers unregistered, wasm frame locked via kicadSetReadOnly failing CLOSED
  (stale bundle → boot error, never a writable frame).
- collab: bindKicadCollab {readOnly} — inert DOWN hook, never seeds a room;
  UP observer + adopt stay live so peer edits render. index.ts / sheet-manager
  thread readOnly + drop initial awareness (invisible observer).
- Reference backend emits access:"write".

Bumps kicad + web/pcbjam-shared to the read-only-viewer commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DN9py5GuPdExaaFzE4k27
2026-07-13 12:48:35 +02:00
Gergő Törcsvári
9315f56760
libs: peer lib edits reach the running editor + "placed symbol updated" toast
synced-source subscribes to its SyncStack: remote changes (self-save echoes
consumed via a selfPushed flag) debounce per kind into kicadLibsReload — a
new embind export (pcbjam_libs_reload.h, all three TUs) that drops the lib's
plugin cache (LIBRARY_MANAGER::ReloadLibraryEntry), reloads it, and mails
MAIL_RELOAD_LIB with the nickname so the symbol tree force-refreshes (the
plugin's modify hash is a pinned constant, so a plain sync would skip it).

After the reload, kicadLibsSymbolUsage (new eeschema embind: placed
SCH_SYMBOL count across unique screens) gates LIB_ITEM_UPDATED_EVENT, and
WasmTool shows an amber toast when a PLACED symbol changed — placed copies
keep the previous version until updated from the library.

syncedScopeLibsSource gives PROJECT sessions the synced source under
VITE_LIBS_SOURCE=synced (remote contract for lib listing/createLib, lazy
per-lib SyncStacks for item ops/presync) so realtime reaches open
schematics; previously project sessions silently fell back to the per-item
remote source. Unit tests cover reload debounce, self-echo skip, per-kind
routing, usage-gated event, and the no-Module no-op.

Bumps kicad (MAIL_RELOAD_LIB force-refresh payload).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QRWoXiM9uuo1enXGhAYku
2026-07-10 09:26:07 +02:00
Gergő Törcsvári
1f85e13acf
feat(collab): follow-user (collab-presence 0008) + chip depth-layer fix
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
  leader's world rect with CONTAIN semantics (zoom derived from the
  follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
  px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
  merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
  awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
  user); applies leader rect changes via FitViewport, dedupes unchanged
  republishes; break-on-interact compares local onViewport echoes against
  the last applied rect (2% rel tolerance, echo-grace before the first fit
  lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
  WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
  both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
  (converge → track → wheel-zoom breaks → subsequent moves ignored).

Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
2026-07-10 09:25:13 +02:00
Istvan Matejcsok
d44ba9bdfb feat(mobile): Figma-like hide-UI toggle — Cmd/Ctrl+\ hotkey + floating button, snapshot-exact chrome restore
Chrome visibility is now a runtime toggle on any device instead of being
device-wired: mobile defaults to canvas-only, desktop to full UI, and the
floating top-right button (or Figma's Cmd/Ctrl+\ chord — free in KiCad,
only bare \ is bound) flips between them live.

- chrome-visibility.ts: module-global store (default isMobileMode(),
  session-only) + pure hotkey matcher (rejects AltGr backslash + repeats)
- WasmTool: capture-phase hotkey (stopped before the wx layer), floating
  toggle pill (matches the comment FAB design), useLayoutEffect apply with
  sync first call + retry; overlays follow the toggle, capability-gated on
  the kicad_editor bundle's kicadSetChrome export
- boot.ts mobile opt now installs touch gestures only
- kicadSetChrome: frame-keyed hide-time snapshot so restore re-shows ONLY
  what hide took away (blanket Show(true) surfaced KiCad's default-hidden
  Search/Properties/Net-Inspector panes); toolbars-only fallback otherwise
- tests: chrome-toggle.spec.ts (desktop hide/restore + geometric
  restore-exactness ±3px), mobile toggle round-trip, 12 new unit tests,
  test:web:mobile npm script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:15:37 +02:00
Istvan Matejcsok
dc7c60f723 feat(mobile): canvas-only mobile mode — pinch-zoom/pan/tap gestures + chrome-less editors
On a mobile device (or ?mobile=1) the editors run canvas-only with touch
gestures driving the view:

- touch-gestures.ts: pure recognizer (unit-tested) + DOM shim installed in
  boot preRun — one-finger drag → synthetic middle-drag (pan), pinch →
  synthetic wheel at the centroid (zoom-to-cursor), tap → left click.
  preRun registration order is what lets stopImmediatePropagation suppress
  the wx layer's single-finger→LEFT-drag touch mapping.
- kicadSetChrome(bool) embind: hides all AUI panes except DrawFrame + the
  menubar/status bar via generic wx APIs (kicad fork untouched); boot polls
  it after runtime init. Pairs with the wxwidgets IsShown layout fix.
- mobile-mode.ts: ?mobile=1/0 override or UA-CH/coarse-pointer autodetect;
  shell hides its overlays and the inherent-to-mobile preflight warnings.
- e2e: mobile-chromium project (Pixel 7) + 4 specs (chrome-less, tap,
  pinch, pan) with screenshot-invertibility assertions; also fixes
  tool-switch.spec's stale pre-scope-refactor URLs (was broken on main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:15:37 +02:00
Istvan Matejcsok
3a2255c817 feat(editor): File→Quit navigates back to the project page
Every KiCad editor's File→Quit used to destroy the wx top frame and
notify nobody, leaving the SPA stranded on the editor URL with a dead
canvas. It now behaves like the browser Back button: return to wherever
the user entered the editor from (project page, or a previous editor).

- WasmTool installs window.wxAppTopWindowClosed (the wx wasm port fires it
  from the main frame's destructor — see the wxwidgets pointer bump). On a
  real quit it history.back()s when there is same-origin in-app history,
  else location.assign()s the project page (or "/" for lib editors).
- The navigation is deferred one task (setTimeout 0): it fires inside the
  C++ destructor via EM_ASM under Asyncify, and a cross-document
  location.assign() started there is aborted by the continuing teardown;
  only same-document history.back() survives synchronously.
- Latches off on pagehide (the port also closes the frame on page unload
  via UnloadCallback) and reloads on a bfcache pageshow so Forward never
  restores a dead frame.
- e2e: quit-to-project.spec.ts covers both the history-back and the
  deep-link fallback paths.

No kicad changes — the whole hook lives in the wxWidgets wasm port and the
web app, keeping the kicad fork clean for upstream syncs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:19:24 +02:00
Gergő Törcsvári
3186986a0a
feat(collab): cross-app selection — eeschema symbol ⇄ pcbnew footprint ghost highlight (collab-presence 0006)
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.

- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
  full PresenceState at selection rate (cursor always null); peers() = other-
  TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
  xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
  cross peers appended to the kicadCollabSetRemote snapshot as
  {id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
  and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
  ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
  room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
  patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
  TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
  merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
  13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
  partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
  (the whole-window #canvas compare flaked on the auto-dismissing version
  infobar — also fixes the long-known presence-eeschema restore flake);
  cross-app + presence-kicad vitest suites

Spec: docs/features/collab-presence/0006 (closed repo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
157d60f6b8
feat(standalone): 8-way parallel lib presync + stable load counters
Library warm-up and the per-item fallback fat-load now run through a
shared asyncMap helper (counting semaphore, 8 in flight): the CDN
presync's hand-rolled 6-worker pool is replaced and the previously
fully-serial listItems+getItemBody fallback is parallelized (order
preserved).

The load-screen sync line stops flashing: progress reports on lib
COMPLETION only (no "current lib" — with 8 in flight there isn't one),
the label is a fixed prefix ("Syncing symbol libraries — ") and the
counter is space-padded to the total's width in font-mono +
whitespace-pre, so 2→3 digit growth doesn't shift the text. Same
padding for the fat-load overlay's "x / y libraries" line.

Verified in demo mode against the live R2 CDN: 7-8 overlapping
manifest/bundle requests, 222 cold symbol libs in a few seconds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
ef15c0d863
feat(collab): ship the tuned presence style as per-editor defaults
Defaults picked with the PresenceTuner (2026-07-07):
- pcbnew: exact-outline selection, 6px stroke @0.7, 46% fill, bottom-end
  chips @7.5px, cross cursor 8/3px @1.0 with chip label, 9px pins w/ 3px ring.
- eeschema (eeschemaDefaultStyle): same look softened for the schematic
  canvas — hairline 1px outline, 14% fill, 0.5 cursor alpha.
- New chipBgAlpha knob (default 0.7 = the border alpha) for label AND cursor
  badges, + tuner slider.
- Tuner: per-tool defaults + per-tool localStorage keys; Reset returns each
  editor to its own shipped look; tool prop threaded from WasmTool.
Verified: pure defaults (no style push) render the picked look in both
editors; presence suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
21e3df8fd0
feat(collab): nth-in-room colors, comment toolbar + drag + global hide, luminance chip text
- presence.ts: colors claimed by ARRIVAL ORDER — each client takes the lowest
  free palette slot (no birthday-problem hash collisions; the best palette
  colors go first; N ≤ palette-size editors always distinct). Coordination-
  free: claims ride awareness; simultaneous-join collisions converge (lower
  clientID keeps, the other re-claims), same-user tabs adopt one color,
  claims are sticky across eeschema sheet rebinds (skeletons reuse them).
  colorOf(userId) resolves live colors; comment pins/popovers follow it
  (offline authors fall back to the hash). +2 unit tests (verified live:
  alice=slot0, bob=slot1, roster/pins consistent).
- CommentLayer: single comment icon expanding into a horizontal bar — new
  comment · list · show/hide all (eye empties the GAL pin set + DOM targets;
  re-shown on new-comment/panel-jump). Pins are DRAGGABLE: live LWW anchor
  writes while dragging (peers + the GAL dot follow), nearest-item re-snap on
  drop, click-vs-drag by 4px threshold (verified live: drag synced to the
  peer tab exactly). Shared setThreadAnchor + controller moveThread/
  setPinsVisible/colorFor.
- collab_presence_style.h: chip text color by background luminance (dark on
  light chips, white on dark — BitmapText draws with the stroke color).
- comments/presence-roster e2e updated for the bar + passing; collab units
  56/56; shared 122/122; shared pointer bump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:23 +02:00
Gergő Törcsvári
ce99773af4
feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
  editor TUs — no more duplicated overlay code): selection shape (rect /
  corner brackets / underline / rounded rect / filled-only), border width +
  alpha, infill alpha, padding, corner radius; name tag show/size/chip-
  background/inside-outside/top-bottom/start-end-center/offset; cursor shape
  (cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
  and palette-by-name-hash overrides (try palettes without changing what
  senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
  (real KIIDs for synthetic previews); merged dispatch; pins now carry the
  author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
  grouped sliders/selects, demo peers+pins injection for SOLO tuning,
  localStorage persistence across reloads, Copy JSON export, reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:22 +02:00
Gergő Törcsvári
635aa2926a
feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
  anchor resolution per tool IU (pins track item moves via kdoc_items
  observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
  via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
  over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
  delete thread), panel with resolved filter + jump-to (popover centers when
  the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
  window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
  filter → delete, passing vs real partykit; presence suites + collab units
  stay green; shared pointer bump (0004 model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:22 +02:00
Gergő Törcsvári
6bc54de92b
feat(collab): presence P3 — eeschema port (collab-presence 0003)
- eeschema_embind.cpp: presence section (0002 pattern, zero fork changes) —
  wx canvas triggers + SCHEMATIC_LISTENER piggyback → post-settle
  SCH_SELECTION_TOOL emit; throttled cursor; remote VIEW_OVERLAY render
  (SCHEMATIC::ResolveItem, name tags, screen-constant via GAL matrix);
  schCollab{PresenceStart,SetRemote,GetViewport,GetSelection,TestSelectFirst,
  TestClearSelection}; kicad_editor_embind dispatches by active frame.
- sheet-manager: parked rooms carry SKELETON awareness states
  ({user,tool,sheetPath=bound sheet}) via publishSkeletons on switch + late
  warm-up — the bound room's awareness then holds every project peer, so no
  multi-room aggregation; presence.ts publishSkeleton helper.
- PresenceRoster: sheet-aware — peers on another sheet render dimmed with
  'on <sheet>' tooltip; WasmTool un-gates the kicad presence bridge for
  eeschema and threads activeSheetPath.
- tests: presence-eeschema.spec.ts (5 e2e, mirrors pcbnew incl. the px/IU
  band at eeschema's 1e4/mm IU); presence.test.ts +2 (skeleton visibility,
  no ghost cursor after rebind). eeschema-collab/subschema stay green.

Verified live: two tabs on demo.kicad_sch — peer cursor cross + label,
selection box + name tag, roster avatar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:22 +02:00
Gergő Törcsvári
8ad1cc673a
feat(collab): presence P2 — pcbnew selection/cursor emit + remote VIEW_OVERLAY render (collab-presence 0002)
Zero kicad-fork changes — all in the embind layer:
- pcbnew_embind.cpp: presence section — canvas wx Bind() triggers (motion/
  leave/up/key/wheel) + COLLAB_LISTENER piggyback → post-settle selection
  check (dedupe) → onSelection; throttled cursor emit via VIEW::ToWorld;
  viewport push/pull (px-per-IU via the GAL matrix — GetScale() is the zoom
  and sized the first cut's overlay nm-small); kicadCollabSetRemote renders
  peers' cursors (cross + name) and selection bbox outlines into one
  per-user-colored VIEW_OVERLAY (CallAfter+COROUTINE), never touching local
  selection; PresenceStart/GetSelection/TestSelectFirst/TestClearSelection.
- kicad_editor_embind.cpp: merged-image dispatch (pcb-only until 0003).
- presence-kicad.ts: bindKicadPresence — routes emits into awareness (0001)
  and pushes trailing-throttled peer snapshots into the wasm; wired from
  WasmTool.startPresence (pcbnew-gated). +5 unit tests.
- tests/kicad/presence-pcbnew.spec.ts: 5 e2e — programmatic + real box-select
  emit, throttled cursor, remote render with no-leak + pixel restore,
  viewport unit band. Existing pcbnew-collab/items-bridge suites stay green.

Verified live: two tabs over partykit — peer cursor cross + label + selection
outline visible on the other tab's canvas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:22 +02:00
Gergő Törcsvári
22528f450b
feat(collab): presence P1 — awareness transport + who's-here roster (collab-presence 0001)
- provider.ts: expose awareness on YjsProvider (partykit/hocuspocus surface
  their own; BroadcastChannel gets a sibling-channel relay via awareness-bc.ts)
- presence.ts: createPresence — publish local PresenceState, subscribable
  deduped peers() roster, setCursor/setSelection for P2, pagehide fast removal
- PresenceRoster.tsx + WasmTool wiring: facepile chip next to SourceChip;
  eeschema rebinds per active sheet (sheet-manager ActiveSheet now carries the
  room's provider); config presenceUser() = slug + colorForUser
- new dep y-protocols; unit tests (presence.test.ts) + e2e
  tests/web/presence-roster.spec.ts (two tabs, verified vs real partykit)
- bump pcbjam-shared: presence-wire schema (MIT)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
2026-07-07 21:09:22 +02:00
Gergő Törcsvári
62ca571802
feat(ysync): lib_symbols travel + layout save-sync (miss 08), TS hot-path opts (12), diff-on-rebind adopt (13) — doc 18
Miss 08A: the binding stores wire-carried (lib_symbols …) definitions in
kdoc_libsymbols and prefixes them on apply wires — a joiner that never saw a
symbol adopts it WITH its definition (new e2e ysync-libsymbols.spec.ts).
Miss 08B: registerSaveHook gains onSavedText; WasmTool routes saved-file text
to syncLayoutToY (per sheet room via the manager's syncLayoutFromSave, or the
single-room doc) so title block / paper / setup edits converge instead of
drifting. Opt 12 (TS half): zod off the observer hot path (yToItemUnchecked),
children index built once per conversion. Opt 13: seed()'s adopt diffs the
editor snapshot against the doc view and applies only the doc-authoritative
difference — clean rebinds apply nothing, the adopt undo entry shrinks to the
real changed set. Opt 14 deliberately deferred (doc 18). All TS-side; no wasm
rebuild (the C++ blob/findLib sides already carried definitions).

Verified: shared 107, standalone 79 (+2 known pre-existing wasm-assets),
ysync e2e 21/21 chromium, collab regression 21/3-skip firefox.

Bumps: web/pcbjam-shared (lib_symbols channel + syncLayoutToY + opts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
2026-07-06 08:57:58 +02:00
Istvan Matejcsok
c6716e6b60 feat(wasm): unify editor builds — footprint/symbol editors via runtime --frame
The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher.

The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:56:02 +02:00
Gergő Törcsvári
6f3f3bcf00
feat(3d): lazy 3D model delivery R2→IDB→WASM
- cdnModelsSource: sparse per-lib stacks over the models CDN layout
  (libs/kicad-models/<tag>/<lib>/manifest + content-addressed blobs)
- models-bridge: board prescan ((model …) scan → prefetch, 4/4 in 74ms
  on the demo board) + the kind=model3d 'ensure' op answering absolute
  MEMFS paths; only board-referenced bodies ever enter IDB
- boot/runner/config/WasmTool wiring (VITE_MODELS_MANIFEST_URL,
  KICAD*_3DMODEL_DIR seeding, prefetch badge)
- StorageUsageCard on HomePage: per-kind cached sizes + delete-3D-cache
- publish-models.ts (brotli content-addressed publish, 606→93MB for the
  pic_programmer lib set) + dev-demo --models-tag/--models-local
- e2e: 3d-viewer-models.spec.ts (bridge normalize/dedup + STEP/WRL
  render); submodule bumps: kicad (static 3D plugins + ensure hook),
  pcbjam-shared (sparse sync layer)

Spec + findings: docs/features/3d-models (private repo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AT7gVHRktDYoQ68S4x6A4
2026-07-02 11:35:36 +02:00
Gergő Törcsvári
88aaba99c9
feat(libs): react load overlay + progress bar; mimalloc mallinfo stub; framed tests
- WasmTool/source.ts: LIB_LOADING_EVENT brackets the fat-load; full-cover
  overlay ('Loading <kind> libraries…') with a per-lib progress bar (done/total
  from listLibs), show-on-first / hide-after-last coalescing.
- mallinfo stub (wasm/shims/mallinfo_stub.c + build-kicad-target.sh): -sMALLOC=
  mimalloc doesn't export glibc mallinfo() that OpenCASCADE OSD_MemInfo (pcbnew
  3D) needs; zeroed no-op unblocks the pcbnew link.
- source/cdn-source tests: updated to the framed Uint8Array 'bodies' contract.
- bump kicad -> e8db3d3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:56:32 +02:00
Gergő Törcsvári
234e5fe189
feat(scopes): phase 0003 — standalone scope routing, tool inference, scoped API client, Demo/Local/Cloud badges
Routes → /:scope/projects/:name/* (+ -/:tool) and /:scope/libs/:name; tool inferred from file ext / lib kind (?tool= override); currentScope()/userSlug() in config; SCOPE/USER headers + scoped paths through project + libs sources; @local/demo scopes on constructed projects; badge relabel (drop scary read-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:54:49 +02:00
Gergő Törcsvári
39cb04b127
feat(standalone): demo follow-ups — waitlist, analytics, move-to-local, file tree, load progress
- 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
2026-06-27 07:59:09 +02:00