Commit Graph

1459 Commits

Author SHA1 Message Date
David Montero Crespo ec233fea36 docs: changelog entries for the Web Serial flash and hardware serial seams 2026-08-11 21:23:56 +02:00
David Montero Crespo 1980d70942 feat(serial): hardware-serial seams for the pro Web Serial monitor
appendHardwareSerial pushes real-UART bytes through the existing
per-frame serial batcher (no SerialMonitor changes), and
lib/proHardwareSerial lets the overlay intercept a board's serial
input while a physical monitor is attached — TX and RX switch to the
hardware together, detaching hands the tab back to the simulator.
Also adds @types/w3c-web-serial (esptool-js references it but only
dev-depends on it).
2026-08-11 21:23:56 +02:00
David Montero Crespo def450f4f1 feat(flash): seam for browser Web Serial flashing (pro overlay hook)
The board context menu's Flash item and FlashModal gain a second
backend: a Web Serial flasher the pro overlay can install via
lib/proWebFlash.ts (installWebFlashImpl). Without an overlay the seam
is inert — web builds keep the desktop-only behavior. esptool-js is
declared here because the overlay has no package.json of its own; OSS
code never imports it (verified absent from the vite bundle).
2026-08-11 21:23:56 +02:00
David Montero 3338e56553 fix(news): pro builds wait for the overlay source before fetching
The announcement fetch now fires 250ms after editor mount — early
enough to race the dynamically imported pro overlay chunk that
registers the real news source. Losing the race silently fell back to
the OSS self-host proxy, which on the cloud deployment returns [] (its
hairpin fetch of its own public feed does not survive the round trip),
so cold-cache visitors — every incognito window — got no announcement
at all. getNextNews() in a pro build now waits, bounded at 8s, for
registerNewsSource() before picking a source; OSS builds resolve the
gate immediately and behave exactly as before.
2026-08-11 20:40:46 +02:00
David Montero 75efd68b85 feat(news): announcement opens as a modal before the starter dialog
The corner toast went unread. The announcement is a modal again — what
makes it viable this time is sequencing, not stacking: a tiny gate
(lib/newsGate.ts) lets EditorPage hold the pristine-visit starter
dialog until the news flow is done (nothing to show, or modal closed),
so only one startup overlay is ever on screen and news always comes
first. The pending decision is bounded at 2.5s so a dead feed cannot
hold the starter dialog hostage; reading time is unbounded.

The modal is deliberately a touch wider than the starter dialog
(880px vs 840px) and the news fetch delay drops 4s -> 250ms since the
starter now waits on it. Toast markup and styles removed; Help >
What's new keeps reopening the last delivered post.
2026-08-11 20:12:37 +02:00
David Montero Crespo fb8086c205 fix(canvas): collapse the Camera/Mic buttons to icon-only when the toolbar is cramped
With the AI chat docked, the canvas header ran out of room and the Camera
and Microphone buttons overlapped the icons before them (visible as
"…Camera" colliding on a XIAO-S3-Sense board). Serial and Scope already
collapse to icon-only via the `@container unified-toolbar` query
(font-size: 0 hides the label, the fixed-size SVG stays), but Camera/Mic
carried their layout in INLINE styles, and an inline font-size beats the
class rule so the query could never reach them.

Move the two toggles' static layout into a shared `.canvas-cam-mic-btn`
class (only state-driven styles — background, border, colour, the pulse
animation — stay inline) and add that class to the same collapse rules as
Serial, in both the container query and the mobile media query. Also drop
the header button gap from 8px to 4px once the toolbar is narrow so all
four (Serial/Camera/Mic/Scope) pack in without overlapping.

Side effect: the toggles now share Serial's 28px height, so the toolbar row
lines up (they were a touch shorter before).
2026-08-11 20:01:56 +02:00
David Montero b8c2b56df2 fix: drop accidentally committed pro-overlay symlink, tighten ignores
b02aab9a picked up frontend/src/pro — a symlink to a private-overlay
checkout that only exists on one machine; every other clone got a
dangling link. The ignore patterns had a trailing slash, which matches
directories but not symlinks — that is exactly how it slipped past.
Drop the slash so both forms stay ignored.
2026-08-11 19:55:34 +02:00
David Montero 6e64027921 feat(news): interaction seam for What's New (impression/open/link_click/video_play)
New lib/newsEvents.ts registry (same pattern as newsSource.ts): OSS
default is a no-op sink; an extension overlay can register one to
forward the signals to its own analytics. NewsAnnouncer reports the
toast impression and modal opens (toast vs Help menu); NewsMarkdown
grows an optional onInteract callback fired on body link clicks and
YouTube preview plays — the admin authoring preview passes nothing, so
authoring never counts as engagement.
2026-08-11 18:37:23 +02:00
David Montero 8f7b878149 fix(canvas): guard the mount-time property loop against the persisted imageData string too
c201f5d9 guarded the property-sync effect, but the mount effect has its
own raw Object.entries(properties) loop that runs first and assigned the
saved imageData: '' onto the fresh element — so the firstUpdated
putImageData crash survived. Same guard, second call site.
2026-08-11 05:45:33 +02:00
David Montero 7ecc791187 fix(canvas): don't clobber a display's live ImageData with the persisted string
Saved projects carry imageData: '' in the SSD1306's properties (the
generated metadata lists it as a text prop). The property-sync effect
assigned that string onto the web component, replacing the element's
live ImageData and crashing wokwi-elements' firstUpdated with
'putImageData: parameter 1 is not of type ImageData' on every project
load. Only a real ImageData may pass through now.
2026-08-11 05:19:31 +02:00
David Montero 49ac373068 fix(sim): drive the slave ACK in the USI I2C bridge (blank SSD1306 on ATtiny85)
The bridge was a passive sniffer that never drove the ACK bit. That
worked by accident: avr8js seeds a pin's external input value to 0, so
when TinyWireM released SDA for the ACK slot the master read 0 (ACK)
and kept transmitting. Commit eb3b04ef (seed INPUT_PULLUP pins to their
resting level) now sets that external value HIGH at init, so every ACK
slot read back NACK and TinyWireM aborted each transmission right after
the address byte: 239 address frames, zero data bytes, zero STOPs, and
a permanently blank SSD1306 on every ATtiny85 + Tiny4kOLED project.

Fix: capture each byte's ACK through the bus master callbacks (the
bridge owns the master slot on the ATtiny85) and hold SDA's external
input low for the whole ACK slot. Timing matters: TinyWireM strobes the
USI with USICS1|USICLK (avr8js clockSrc 3), which samples DI at the top
of the FALLING USITC write, one toggle after the 9th rising edge, so
the drive is released only on the falling edge that closes the slot.
Unowned addresses now read back NACK like real hardware.

Regression test replays TinyWireM's exact register sequence (from the
ATTinyCore disassembly) against a real AVRSimulator.
2026-08-11 05:19:31 +02:00
David Montero Crespo f03f80d1b3 fix(micropython): sleepy isconnected()/status() — busy-waits starved idleSkip
The canonical MicroPython connect idiom is `while not wlan.isconnected():
pass` — a pure Python busy-wait, and most gallery WiFi examples use it.
The emulated CPU only fast-forwards through WAITI idles, so a busy-wait
pins the sim at real execution speed (~2% of the chip in a browser tab).
Measured on the classic-ESP32 engine with a marker-bracketed harness: the
connect+DHCP phase costs ~1.5M busy instructions under a sleepy poll loop
and ~194M under the busy-wait — same firmware, same radio, 130x apart.
At browser speed that is close to a minute of wall clock for the connect
alone, which reads as "WiFi hangs" and got diagnosed as exactly that.

The proxy now sleeps 20ms per unanswered isconnected()/status() poll,
turning the user's busy loop into an idle loop without touching their
code. Re-measured end-to-end with the real prelude: 226.8M -> 64.3M busy
steps, and the made-up-SSID example connects with a 10.13.37.x lease on
the in-browser engine.
2026-08-10 22:58:30 +02:00
David Montero Crespo d527b1839c fix(micropython): real WiFi driver only where the backend can serve it
Deploy verification of the #262 fix caught a backend split the issue
could not see. The reporter runs the OSS Docker image, where MicroPython
executes under the QEMU worker — there the real driver works and their
fix verification (DHCP on 192.168.4.x, DNS, TCP) holds. But velxio.dev
routes a plain ESP32 MicroPython run to the in-browser esp32js engine,
and the engines' P4 WiFi (which serves Arduino fine) does not yet cover
MicroPython's v1.20 / esp-idf-4.2 vintage esp_wifi blob: handing it the
real module stalls esp_wifi_init with NO output — observed live as four
minutes of dead serial after the raw-REPL paste, strictly worse than the
stub's fake-but-responsive answers.

So the prelude now asks the bridge: an overlay bridge that keeps the
MicroPython run in-browser declares micropythonWifiSupported=false and
keeps the stub; the QEMU path (every OSS install, and ?esp32sim=qemu in
the overlay) gets the real driver + SSID-rewrite proxy. When an engine
gains MicroPython WiFi it flips its declaration and this file needs no
further change.

Property is duck-typed and optional; the plain OSS Esp32Bridge does not
define it, and undefined means supported — OSS behavior is exactly the
reporter's verified environment.
2026-08-10 22:34:49 +02:00
David Montero Crespo ea1cdffb56 fix(serial): linkify bare emulated-subnet IPs, not only http:// URLs
The serial monitor turns printed device URLs into IoT-gateway links —
but only when the sketch printed a full http:// URL. That matched the
Arduino idiom and missed MicroPython's: `print("Open:",
wlan.ifconfig()[0])` emits a bare "192.168.4.15", which rendered as
plain text. The address is unreachable from the host by design (it only
exists inside the emulated network), so exactly the users who just
gained real WiFi were handed an IP with no way to open it.

The scheme is now optional; a negative lookahead keeps longer
dotted strings (192.168.4.15.99) from half-matching. ifconfig() tuples
linkify every same-subnet address they contain — all of them route to
the same gateway root, so the extra links are harmless.
2026-08-10 22:11:04 +02:00
David Montero Crespo f96d239a1d fix(micropython): route unknown SSIDs to the emulated AP instead of hanging
Follow-up to the #262 fix. Handing MicroPython the real driver exposed a
population the stub had been quietly carrying: sketches that connect to
an SSID that simply does not exist in the emulated radio. The AP tables
(QEMU esp32_wifi_ap.c and the esp32*js engines — same four networks in
both) associate by MAC/channel, so an unknown SSID scans to nothing and
`while not isconnected():` spins forever. Most 100-days gallery examples
ship their author's home SSID, so "real driver, no shim" would have
frozen them on the connect loop.

So when a NIC is attached the prelude now injects a thin proxy around
the REAL network module: connect() to any SSID outside the emulated set
is redirected to Velxio-GUEST (announced on serial, so nobody debugs a
connection they didn't make); known SSIDs pass through untouched; every
other attribute delegates to the native module. Result: made-up-SSID
sketches get genuine DHCP/DNS/TCP through SLIRP instead of the old fake
answers, and the reporter's PICSimLabWifi repro is never rewritten.

Wrapper semantics (rewrite, passthrough, module-attr delegation, and the
`from network import WLAN` form) are exercised against an instrumented
fake module; both generated preludes are syntax-checked.
2026-08-10 21:52:30 +02:00
David Montero 0653aad3d4 fix(picker): hide the Nano RP2040 Connect shell — art without a simulator
The metadata auto-scan sweeps every wokwi-elements board into the
registry, and boards Velxio does not emulate surface as component cards
that promise support we don't have. Filtered in the picker, not deleted
from the metadata: four saved projects already contain the element and
must keep rendering.
2026-08-10 21:45:05 +02:00
David Montero Crespo 1800f57225 fix(micropython): stop shadowing the real WiFi driver when a NIC exists
Reported in #262: on an emulated ESP32, MicroPython's WLAN.scan() always
returned [], ifconfig() reported 10.0.2.15/10.0.2.2/10.0.2.3, and
outbound sockets raised EHOSTUNREACH — while Arduino WiFi.h on the same
board and container scanned, connected on 192.168.4.x and completed real
HTTP requests. Two independent faults, both needed for the failure.

1. The MicroPython prelude injected a stub `network` module into
   sys.modules unconditionally, so user code never reached the firmware
   driver. The stub's hardcoded ifconfig() happens to be QEMU's SLIRP
   defaults, which is why the symptom read so convincingly as a routing
   bug.

2. hasWifi survived a language switch. It is a property of the compiled
   sketch, set only by arduino-cli, but setBoardLanguageMode kept it — so
   a board compiled once as Arduino carried that verdict into a later
   MicroPython run. The source-scanning fallback only fires when hasWifi
   is undefined, so it never got a chance, and the backend attaches the
   `-nic` device only when wifi_enabled is true.

The stub is NOT removed, which is where this departs from the patch in
the issue. It exists for a reason the report didn't have: without a NIC,
`network.WLAN(STA_IF)` hangs forever waiting on peripheral status bits
QEMU never sets, tripping the FreeRTOS watchdog after ~26s. Deleting it
would trade a fake-but-working network for a hung board. It is now
conditional — real driver when a NIC is attached, stub when none is —
so both paths are honest.

Also kept: the ntptime and urequests stubs, which the issue's diff would
have dropped along with `network` (it removed the whole prelude block).
Those back 29 call sites in the 100-days gallery examples, which have no
emulated internet to reach regardless of the NIC.

The detector is now one shared function instead of two copies that could
disagree — the stub decision and the -nic decision MUST match. Folded in
`from network import WLAN`, which matched neither previous pattern: such
a sketch was judged WiFi-less and hung on the missing peripheral. The
existing tests re-implemented the detection inline, so they only ever
proved that copy self-consistent; the new ones call the shipped function.
2026-08-10 21:40:58 +02:00
David Montero 2908726007 feat(editor): the starter picker offers the Nano family and the ESP32-CAM
The Arduino section listed only Uno and Mega — Nano and Nano ESP32
existed as first-class boards (kinds, FQBNs, examples) but could only be
reached through the gallery or the board dropdown. Same for the
ESP32-CAM, which now sits in the ESP32 section next to its DevKit
sibling. The camera board's starter is the webcam demo rather than a
blink: the board exists to show the sensor, and a bare LED sketch would
be a misleading first run.
2026-08-10 21:33:55 +02:00
David Montero ce27ae3701 chore(examples): drop thumbnails for boards that have not launched
The post-deploy thumbnail bot captured examples for boards still hidden
behind launch control and pushed the files here; the images are blank
canvases but the filenames name unannounced products. The capture leak
is fixed at the source (the gallery's launch filter now checks
boardType/boardFilter), so these cannot regenerate.
2026-08-10 21:25:21 +02:00
velxio-deploy 7ae44f3b14 chore(examples): refresh 13 thumb file(s) [auto] 2026-08-10 20:51:38 +02:00
velxio-deploy 36faa4c5a8 chore(examples): refresh 20 thumb file(s) [auto] 2026-08-10 20:33:55 +02:00
David Montero Crespo 99471d0657 feat(news): announce with a toast, open the modal only on request
Entering the editor could stack two overlays: the starter-template
dialog opened on mount, then four seconds later the "What's new" modal
landed on top of it. Two dimmed backdrops over each other, and the
smaller, *less* important surface in front — the z-indexes said it
outright, news 10000 against the starter's 9000.

The delay made it worse rather than gentler. Arriving after the user has
started reading the templates is the most disruptive moment available;
appearing instantly would at least have let them clear it before
engaging.

The rule this restores: a modal is for what BLOCKS, and an announcement
never blocks. News now slides into the corner as a toast that dims
nothing, steals no focus (role="status", polite), and retires by itself.
The full post is one click behind "Read more" — a modal the user asked
for is a categorically different thing from one thrown at them — and
stays reachable from Help ▸ What's new once the toast is gone.

Worth noting the audience argument too: the starter dialog only fires on
a PRISTINE visit, i.e. for someone brand new, and "what's new" is
meaningless without a baseline to compare against. Returning users, the
ones it is actually written for, arrive with a project loaded and never
saw the collision.

Renamed NewsModal → NewsAnnouncer: it is not a modal by default any more,
and leaving the old name would have mislead the next reader.

Strings are new keys under `news.*`, filled in for all nine locales — the
previous modal had "What's new" and "Got it" hardcoded in English.

Toast sits at z-index 8000, deliberately BELOW the starter dialog, so if
the two ever coincide again the blocking task keeps the foreground.
2026-08-10 19:52:55 +02:00
David Montero Crespo 28eca48168 feat(editor): session entry points in the File and Account menus
The editor menubar's Account menu hosted only the shared `user-menu`
slot, so it had no way in or out of a session: the bottom-left dropdown
renders My projects / Sign in from its own markup, which this menubar
never sees. Signed-in users had no route to their saved work from the
editor, and anonymous ones had no way to sign in without leaving it.

Declares two account-scoped command ids and adds an `optional` item flag
that HIDES a row whose command is unregistered instead of the default
"render disabled". Absence here is not "temporarily unavailable" but
"does not apply": OSS has no accounts, and in pro exactly one of the two
is meaningful at a time — a greyed-out My projects would read as broken.

My projects sits third in File, at the head of the project group rather
than tacked onto the two "new …" rows: it opens saved work, which is
what Open/Save beneath it are for.

Labels reuse header.auth.{myProjects,signIn}, already translated in all
nine locales — no new strings.
2026-08-10 19:47:20 +02:00
velxio-deploy 8fd81fa080 chore(examples): refresh 20 thumb file(s) [auto] 2026-08-10 18:16:37 +02:00
David Montero Crespo 7dde3162f7
Merge pull request #266 from davidmonterocrespo24/v3.2
V3.2
2026-08-10 12:31:40 -03:00
David Montero Crespo 044b619d18 fix(ci): cap mcp below 2.0 — the backend suite has not run since 2026-07-29
mcp 2.0.0 shipped on 2026-07-28 and deleted mcp.server.fastmcp, the module
app/mcp/server.py imports. requirements.txt asked for `mcp>=1.0.0` with no
ceiling, so the very next CI run resolved the new major and died importing
test_mcp_tools.py.

That failure is a COLLECTION error, not a test failure: pytest aborts with
exit 2 before running anything, so all 295 unit tests have been silently
unrun on every push since. The workflow was red, but red for a reason
nobody's code caused.

The deploy venv escaped only by accident — it was built while 1.x was still
current, and deploy.sh rebuilds it when requirements.txt changes, so the
next rebuild would have walked into the same wall and taken the deploy gate
down with it.

Verified in a clean venv built from this file: resolves to mcp 1.29.0,
322 passed / 18 skipped, test_mcp_tools.py's 35 tests among them.

Raising the cap means porting to the 2.x server API (mcp.server.mcpserver).
2026-08-10 17:14:39 +02:00
David Montero Crespo 0b875d4440 Merge remote-tracking branch 'origin/master' into v3.2-worktree
# Conflicts:
#	backend/app/services/espidf_compiler.py
#	frontend/src/components/editor/EditorToolbar.tsx
#	frontend/src/utils/vlxFile.ts
2026-08-10 17:00:24 +02:00
David Montero Crespo 7a7c9f93f7 feat(simulator): tilt and battery controls for boards that carry an IMU or a gauge
The emulated BMI270 and battery gauge were faithful and inert: with nothing
driving them the IMU reported the board lying flat for the whole run and the
gauge reported the single voltage it was seeded with, so a sketch that reacts
to tilt or warns on low charge could not be exercised at all.

A Sensors popover on the canvas header supplies the missing half. Tilt is
entered as roll/pitch on a drag pad rather than three raw axes — what a user
means is "lean it left" — and gravity in the board's frame is that rotation
applied to (0, 0, 1) g, which is also why the vector stays at 1 g whatever the
attitude. Turning the board spins the gyro for as long as the drag lasts and
parks it back at zero shortly after, matching a real part: a held tilt reads no
rotation. Battery is a slider over the LiPo working range.

Gated per board by builtInImu / builtInBattery on the pro board def, the same
way the Mic toggle is gated, and each section renders only if its flag is set.

The microphone stream also learns to resample. The engine now tells the source
which rate the guest programmed, so a sketch recording at 8 kHz consumes two
captured samples per read instead of playing back at half speed.
2026-08-09 23:48:56 +02:00
David Montero Crespo d446031674 feat(news): bare image URLs render inline in post markdown
Authors paste screenshot URLs without knowing the ![](...) syntax; GFM
autolinks them into plain <a> tags, so videos previewed but images did
not. An autolinked href ending in an image extension now renders as the
image itself; explicit [label](...png) links stay links. All images get
loading=lazy.
2026-08-09 23:12:56 +02:00
David Montero Crespo c0d3543573 feat(metrics): compiles and runs remember which gallery example they came from
useProjectStore.currentExampleId — stamped by loadExample, dropped the
moment a real project (or a blank workspace) takes over. The compile
request and the run event carry it as example_id, analytics only, so
'which examples get compiled and run most' becomes answerable.
2026-08-09 22:34:26 +02:00
David Montero Crespo 27f712444b feat(store): hydrationSeq — bulk canvas loads identify themselves
setComponents/setWires (project load, example load, .vlx/wokwi import,
clear) bump a counter so observers that diff components/wires between
updates can tell hydration from a user deliberately adding one part.
Canvas edits (addComponent/addWire) don't touch it.
2026-08-09 22:28:16 +02:00
David Montero Crespo 93314b074b fix(esp32sim): re-sync the flat simulator field after a bridge rebuild
Second half of the deep-link race: an overlay board kind added BEFORE
the overlay registers makes isEsp32Kind() false, so addBoard wires the
board as a generic AVR — and the parts on the canvas attach their I2C
device models to THAT simulator (measured live: the gesture's device
sat on an AVRSimulator while the C6 engine booted with an empty bus).
The overlay's rebuild then swapped simulatorMap but left the flat
`simulator` field stale, so DynamicComponent's [simulator] effect never
re-attached anything. Re-syncing the flat field when the rebuilt board
is the active one makes every part re-attach against the real shim —
sensors, displays, mic sources, the lot.
2026-08-09 22:27:57 +02:00
David Montero Crespo 77e947ef98 fix(esp32sim): a rebuilt board adopts the I2C devices its parts attached
The pro overlay loads async, so a deep-linked example attaches its part
device models to the shim wired around the STOCK QEMU bridge — whose
attachSyncI2cDevice is a silent optional no-op — and only then does the
overlay's factory rebuild the board with the js-engine bridge. The
rebuild made a brand-new shim; the parts on the canvas hold the old one
in their attach closures and never re-attach. Result: the engine booted
with an EMPTY I2C bus and arduino's i2c-ng driver failed every
transaction with ESP_ERR_INVALID_STATE (the seated C6 gesture example —
measured live with the bus device table at Map(0)). Whether it broke
was a chunk-load race; the pro bundle's growth turned it into a sure
loss.

The shim now records every sync-attached device and a rebuilt shim
adopts its predecessor's — through addI2CDevice, so the new Delegating
bridge buffers them pre-connect and its start() replays them into the
engine after reset, as it always did for attaches it actually heard.
2026-08-09 22:06:51 +02:00
David Montero Crespo 8b2715affd feat(metrics): compiles and runs carry the editor BoardKind; ads learn to stay quiet
Several boards share one FQBN (the Pimoroni RP2350 trio all compile as
rpipico2), so FQBN-keyed metrics can't tell them apart. The compile
request and the run event now carry board_kind — analytics only, rides
into the metric hook's extra.

suppressOnlineOnlyAds(): overlay seam so the hosted build can silence
'available in the online editor' ad cards for items it manages — an
embargoed item must vanish, not advertise the site the user is on.
Pure OSS builds never call it; ads behave exactly as before.
2026-08-09 20:48:11 +02:00
David Montero Crespo f791536007 fix(wires): close both halves of the load-time endpoint race
The whenDefined recalc was necessary but not sufficient — measured on
staging, the example route loses BOTH ways:

- the element side: every settle timer can fire before the part is
  findable in the DOM. The pinInfo-ready poll in DynamicComponent now
  also re-derives wire endpoints the first time the part's pinInfo is
  actually readable, and waits up to 10s (the 2s cap was the corner-
  wire bug again on slow connections).
- the wire side: loadExample stores wires with zeroed endpoints BEHIND
  awaits (library installs), potentially after every recalc has come
  and gone. A canvas effect now spots endpoints still at (0,0) and
  runs one deferred recalc; real coordinates land, the condition
  clears, no loop.
2026-08-09 08:32:20 +02:00
David Montero Crespo 4b9fa3f0d0 feat(datasheet): Product-page links carry UTM attribution and report the click
Both Buy renderers (picker hover panel + Part Inspector) now append
utm_source=velxio&utm_medium=simulator&utm_campaign=datasheet with the
component id in utm_content, so vendors can attribute the referral.
trackProductPageClick fires the GA4 event and dispatches a
'velxio:product-page-click' CustomEvent — an overlay build can persist
the click server-side without this tree knowing about it.
2026-08-09 08:16:47 +02:00
David Montero Crespo 75842e3668 fix(wires): recalculate endpoints when a late-defined element upgrades
The canvas re-derives wire endpoints on load-settle timers (100/300/
500ms), but an overlay-defined custom element can upgrade after the
last one — its chunk is large — and calculatePinPosition then finds no
pinInfo, so every wire to the part falls back to its corner and floats
there until something moves it (field report: the Grove gesture module
with all four Grove wires dangling on bare PCB). A customElements
.whenDefined hook now recalculates when the tag actually defines,
however late that is.
2026-08-09 08:13:48 +02:00
David Montero Crespo 6d6d298130 fix(console): the final flush speaks in whole lines, from the real buffer
tailAfterStream sliced result.stdout at the live-buffer offset, but the
buffer interleaves stdout+stderr in arrival order while result.stdout is
stdout-only plus a joining newline — offsets never line up, so the flush
could start mid-line ('f/esptool_py && ...'). Instead, the poll loop now
emits one final onProgress(state:'done') with the completed buffer and
the existing delta logic flushes the tail perfectly aligned. tailAfterStream
is gone.
2026-08-09 08:05:43 +02:00
David Montero Crespo 23784568f6 fix(console): flush the stdout tail the last status poll never saw
With the success-path reprint gone, the lines that land between the
final running-poll and job completion (esptool image creation, the
binary-size summary) silently vanished. tailAfterStream() appends
result.stdout past the streamed offset, noise-filtered, before the
structured analysis — skipped once the live buffer hits its 256 KB
tail-trim cap, where offsets stop lining up.
2026-08-09 07:52:46 +02:00
David Montero Crespo 0a868cc254 fix(console): the compile log tells the story once, without cmake's diary
ESP-IDF builds sprayed the compilation console with configure chatter
(-- cmake status, git-metadata probes that 'fatal' on the synthesized
project tree, IDF component-manager NOTEs, Python env checks) and then
reprinted the entire stdout a second time when parseCompileResult ran
over the final result after the live stream had already shown it.

- isNoiseBuildLine(): pattern list for build-system chatter, applied to
  the live stream deltas and to the final-result parsing.
- parseCompileResult(streamedLive): skips the stdout reprint on success;
  on failure re-emits only error/warning lines so they still get the
  red/yellow markers the plain live stream lacked.

Compiler errors, FAILED blocks, ninja [N/M] progress, esptool output and
binary-size summaries all still show. The full log stays available
server-side.
2026-08-09 07:36:36 +02:00
David Montero Crespo 980c564f66 Revert "ux(editor): the pristine starter picker is a screen, not a modal"
This reverts commit c93d954169655d5f6f6a4cc1a68ad31b4cdd77e4.
2026-08-08 06:24:47 +02:00
David Montero Crespo fccb51f3f0 Revert "fix(starter): the takeover cedes the agent chat's docked width"
This reverts commit eeb4480dc37f3d62e94f704ebe11c37a706ef5e9.
2026-08-08 06:24:47 +02:00
David Montero Crespo 15654b664b fix(starter): the takeover cedes the agent chat's docked width
The chat reserves its width as padding-right on .app-container, but an
absolute inset-0 layer spans the padding box, so the picker's right
column and its scrollbar landed under the panel. The screen now gives
up the same width through the same CSS variable, with the same 0.12s
ease the padding uses, and falls back to zero in builds without the
overlay.
2026-08-08 05:39:23 +02:00
David Montero Crespo 3dd780503f ux(editor): the pristine starter picker is a screen, not a modal
A modal over a pristine /editor dims an interface that is empty anyway
and offers a Cancel whose only destination is a dead workspace. The
picker now takes over the content area (StarterScreen): no veil, no
Cancel, the grid gets the room the 840px dialog never had, and the
examples gallery link is the secondary path out. The modal stays for
"New workspace" over existing work, where the replace-everything
warning and Cancel mean something — both shells share one card grid.

The details that make the takeover behave like a page: it sits at
z-index 90 (above the canvas' floating zoom pill, below the header's
stacking context so the menu dropdowns stay clickable; the fixed
account corner box is render-gated instead), the panes underneath are
inert while it is up, focus lands on the heading on mount and parks on
the workspace container on dismissal, and any path that fills the
workspace meanwhile — the File-menu modal, Open project…, a dropped
.vlx — dismisses it rather than letting its cards silently wipe the
loaded work. Full editor.newProject i18n for all nine locales (German
in Sie-form), heading outline h1→h2 / h3→h4, token focus ring, and
reduced-motion respected.
2026-08-08 04:53:36 +02:00
David Montero Crespo 027a6055a4 ux(picker): maker-first ordering — passives, analog and logic gates sink to the end
The component grid and the category filter now follow a fixed
maker-first category order (sensors, outputs/LEDs, displays, input,
motors, communication, electromech, other… then passive, analog, logic):
Velxio's audience reaches for a DHT22 or an LED far more often than a
bare transistor or a 74HC gate. Stable sort — order within a category is
untouched, and search results group the same way.
2026-08-08 04:15:10 +02:00
David Montero Crespo 9d38beae0e ux(menubar): Account menu replaces the top-level Language menu
The account options that only lived in the bottom-left user button
(subscription, licenses, simulation history, replays, privacy) are now
reachable from the header too: a new Account menu hosts the pro items via
the SAME data-velxio-slot="user-menu" the corner dropdown uses — one
overlay renderer, two hosts, empty in OSS builds. Language moves from its
own top-level trigger into a section of that menu, so the menubar stays at
five triggers and frees header room for when the AI chat docks.
2026-08-08 04:02:32 +02:00
David Montero Crespo f9a9657b5e ux(canvas): zoom controls float on the canvas' bottom-right corner
The canvas header runs out of room when the AI chat panel docks on the
right — the board selector clipped and the zoom cluster was squeezed out
entirely. The −/%/+ cluster now floats on the circuit canvas' bottom-right
corner (always free), with stopPropagation so a click never starts a pan.
2026-08-07 23:53:48 +02:00
David Montero Crespo cd83813a9b fix(picker+inspector): dedupe Pi cards, wire Zero/1/2 as real boards, readable many-pin inspectors
Component picker: registry entries that ARE boards (the injected Raspberry
Pi family) no longer render as component cards — clicking one dropped a
dead 40-pin canvas prop that never boots (that is how "add a Pi 4" looked
like it placed a Pi 3). The boards row is the only path now, and it gains
Raspberry Pi Zero / 1 / 2 cards (Pi-3 illustration, PRO pill, same gate):
their QEMU profiles already existed in qemu_manager; the canvas renders
them through the Pi-3 element and BoardOnCanvas/store route them like any
Pi kind.

Part inspector: a board whose canvas node is NOT a registered custom
element (the Pi family's plain <img> wrapper) now clones the live node for
the preview instead of giving up — the Pi 3/4/5 pin map was completely
missing before. Wide boards with 40+ pins (Mega, Pi family) render
ROTATED 90° so their long pin rows become side columns with vertical room
(new PINS_ART_ROTATED_H box), and pinInspectorLayout splits a dual-line
header pair to OPPOSITE gutters the way printed pinout posters do — 40
interleaved labels never fit one gutter. Existing 18 layout tests pass.
2026-08-07 23:03:05 +02:00
David Montero Crespo 32b11d83ec fix(compile-output): IDF "Project commit: HEAD-HASH-NOTFOUND" banners are not errors
esp-insights/esp-rainmaker print their version banner to stderr during the
CMake configure; without git metadata in the component tree it reads
"Project commit: HEAD-HASH-NOTFOUND" and the stderr classifier's default
painted it as a red error. Users read it as a failed build (it is not).
Classify "project commit:" lines as info.
2026-08-07 22:58:39 +02:00
David Montero Crespo e4e4331010 ux(canvas): component camera toggles say "Camera", like the board toggle
The header spoke two languages: the ESP32-CAM/Sense board toggle says
"Camera" while component-owned webcams printed their registry label
("HuskyLens cam", "Gesture cam") — which read as a different control.
The button now says "Camera" everywhere; the component identity stays in
the tooltip, where it disambiguates when several cameras coexist.
2026-08-07 22:46:00 +02:00