Commit Graph

1570 Commits

Author SHA1 Message Date
David Montero Crespo 03b2189043 fix(picker): ad-card art matches the real boards
The VoCat card was a rounded rectangle - the product is a round puck
with two cat-ear bumps, a mic in each and the green LED between them
(official product photo). The P4 card was maroon; the Function-EV PCB
is black.
2026-08-20 03:31:14 +02:00
David Montero Crespo a7db031e73 fix(espidf): one include root per library — kill the header-shadow class
The merged component copied every library into ONE interleaved tree and
then exported EVERY directory that held a file. A library's private
internals therefore sat on the global -I and hijacked #includes issued by
other libraries, by the arduino-esp32 core and by ESP-IDF itself. Each
incident was patched by adding a name to _SHADOW_STD_HEADERS — chasing
symptoms, since the denylist grows with every library that ships a
generically-named internal header (three landed yesterday alone:
FastLED's platforms/stub/Arduino.h, its fl/stl/asio/http/http_parser.h,
and earlier LovyanGFX's lgfx/internal/limits.h).

Adopt arduino-cli's model instead — the resolver every other Velxio board
family already delegates to. It shares one global -I list too, but adds
exactly ONE directory per library: <lib>/src for the recursive layout,
<lib> for the flat legacy one, never a subdirectory; utility/ stays
private to its owner. arduino-esp32 does the same as an IDF component.

  - Each library is copied into its own subtree of the component, so two
    libraries shipping the same relative path no longer collapse into one
    silently-picked copy.
  - INCLUDE_DIRS is one root per library, in resolution order. Internals
    stay copied and reachable through file-relative includes.
  - Legacy-layout utility/ dirs go in PRIV_INCLUDE_DIRS.
  - Root-level header collisions (the only shadowing still possible) are
    logged the way the Arduino IDE reports them, instead of being silent.
  - The transitive scan now walks only the library just copied.

One component is deliberate: a component per library would swap this
failure mode for REQUIRES-order shadowing (IDF exports component includes
as plain -I, never -isystem) plus IDF component-name collisions, and buys
no caching ccache does not already provide.

Verified against the real FastLED+U8g2 project WITH the two denylist
entries removed: green, byte-identical firmware size. The 3-library
SSD1306+GFX+BusIO corpus is green too. VELXIO_PER_LIB_ROOTS=0 restores
the legacy layout; the mode is folded into the build-variant hash so the
two can never share a configured build dir.
2026-08-20 00:15:51 +02:00
David Montero Crespo 2a4d19e9c2 fix(esp32): matrix keypad — active-row model, idempotent install, gap-free listeners
Validated live against the ESP32 alarm-keypad project. Three fixes over
the initial implementation:

- Active-row model: the write-pin callback only fires on OUTPUT-latch
  changes (after the first pass re-driving the row LOW is a no-change
  write), and dir alone misses the first pass. A matrix scan drives one
  row at a time with the latch LOW, so 'the row that most recently went
  active (dir=OUTPUT, or latch write LOW)' IS the scanned row.

- Idempotent install: the canvas effect re-attaches on every boards[]
  mutation (each serial line); reinstalling wiped the pressed-key set
  mid-scan. Same wiring now keeps the existing state dict.

- Gap-free listeners: the keypad's button-press listeners were attached
  on a 300ms setTimeout per effect run, opening a listener-less window
  on every re-run in which a press was lost entirely. Attach
  synchronously and drop the sendSensorDetach cleanup (detach+reattach
  on unrelated re-renders is exactly the wipe the idempotent install
  guards against).
2026-08-19 22:19:27 +02:00
David Montero Crespo 5606e7ebea feat(esp32): server-side matrix keypad — columns driven synchronously in QEMU
A 4x4 membrane keypad scan is us-timing-critical: firmware drives one
row LOW and reads the columns back within the same loop pass, far
faster than the browser<->QEMU websocket round trip, so the browser-side
matrix simulation could never register a key on ESP32 boards.

Worker: new 'matrix-keypad' sensor type (rows/cols GPIO lists, pressed
key set). _on_dir_change/_on_pin_change recompute the column levels
synchronously on the QEMU thread — the pinMode(OUTPUT) transition is
the trigger on steady-state passes, since the row's output latch stays
LOW and the digitalWrite fires no change callback. Async gpio_in writes
to keypad-owned columns are ignored so the browser's generic matrix
logic can't clobber them.

Frontend: SimulatorCanvas attaches the keypad as a matrix-keypad sensor
when every R/C pin resolves to an ESP32-family board GPIO, and streams
the pressed-key set from the element's button-press/release events.
2026-08-19 20:44:00 +02:00
David Montero Crespo 13cbcf5ed3 fix(espidf): run library resolution/merge off the event loop
_resolve_library_components does minutes of synchronous filesystem work
for a big library (FastLED: BFS over every header + copying thousands of
files) directly inside async compile(), starving the event loop: the
/api/compile/start response never flushed and clients timed out at 30s
while the build itself was healthy. Run it via asyncio.to_thread like
the cmake and ninja steps already do.
2026-08-19 20:25:01 +02:00
David Montero Crespo 33ee71ef21 fix(espidf): shadow-exclude http_parser.h — FastLED's copy hid the IDF enum header
IDF's esp_http_server.h includes <http_parser.h> for the HTTP_GET/...
method enums. FastLED ships fl/stl/asio/http/http_parser.h and the merge
put that directory on the global -I, so the IDF include resolved to
FastLED's file and every httpd_method_t use failed with 'HTTP_GET was
not declared'. FastLED only references its copy path-qualified, so the
directory can safely stay off -I.
2026-08-19 20:16:12 +02:00
David Montero Crespo d0ea4cbf8f fix(espidf): concatenate ALL .ino tabs into the sketch (Arduino semantics)
The arduino-mode branch used only the FIRST .ino in the request and
silently dropped every other tab, so any multi-tab sketch (the standard
Wokwi/Arduino-IDE project layout) died with 'not declared in this scope'
on globals defined in the main file. Concatenate the main sketch
(literal sketch.ino, else the first .ino) followed by the remaining
tabs alphabetically — the arduino-cli order — with #line markers so
diagnostics keep pointing at the real tab/line. Also REQUIRE
http_parser: exposing esp_http_server.h activates FastLED's fl net
server, which uses the HTTP_GET/HTTP_POST enums http_parser provides.
2026-08-19 20:10:13 +02:00
David Montero Crespo 0368d24aab fix(espidf): broaden merged-component REQUIRES to the IDF components FastLED reaches
esp_cache.h (esp_mm) was the next one-header-at-a-time failure after
esp_http_server. Scan FastLED 3.10's esp platform tree once and add the
full candidate set (esp_mm, esp_hw_support, heap, esp_system, esp_rom,
log, app_update, bt, esp_psram, esp_pm) — every entry remains guarded on
the component existing in the IDF tree, so older IDF generations are
unaffected.
2026-08-19 20:03:26 +02:00
David Montero Crespo 147694f2d7 fix(espidf): demote nested junk-named dirs to header-only; require esp_http_server
Two more merge gaps surfaced by FastLED 3.10 + U8g2 in one sketch:

- src/fl/test/ was fully dropped because 'test' matched the generic
  excluded_dirs check at ANY depth, but FastLED's unity build includes
  "fl/test/_build.cpp.hpp". Repo-level dirs named test/examples/docs
  stay fully excluded; the same names nested inside a src-layout tree are
  now header-only (fragments copied, sources still never compiled).

- FastLED's fl/net remote code includes <esp_http_server.h>, which the
  Arduino IDE provides via the core's global include path. Add
  esp_http_server to the merged component's guarded REQUIRES list.
2026-08-19 19:56:45 +02:00
David Montero Crespo b6e84c5d71 fix(espidf): copy extras/ header fragments into merged libs (never their sources)
FastLED's unity build (src/fl/build/extras+.cpp) includes
"extras/_build.cpp.hpp", but extras/ was in the excluded_dirs set so the
fragment was never copied and the merged-component build died with
'No such file or directory'. Treat extras/ as header-only instead: copy
.h/.hpp/.inl fragments so unity includes resolve, but keep compiling
nothing from it — extras/ traditionally holds repo-only content.
2026-08-19 19:51:10 +02:00
David Montero Crespo 39b2ccf515 fix(espidf): keep dirs shipping an Arduino.h shadow header off the merged -I path
FastLED ships a host-test stub src/platforms/stub/Arduino.h. When another
merged library (e.g. U8g2) does #include <Arduino.h>, the stub resolved
first and broke the build: 'yield' was not declared in U8g2lib.cpp and
fl::string polluted String overload resolution inside the core WString.h.
Reuse the _SHADOW_STD_HEADERS mechanism so the stub's directory stays off
the global include path; FastLED reaches it via a file-relative include.
2026-08-19 19:41:23 +02:00
David Montero Crespo 3558d9ba39 feat(espidf): honor CDCOnBoot menu option in 4-part FQBNs
arduino-cli FQBNs may carry a menu-option suffix
(vendor:arch:board_id:Menu1=val1,...). The espidf lane previously derived
cdc_on_boot exclusively from the board id's boards.txt default, so a
generic-devkit board could never be built with Serial on the USB
Serial/JTAG CDC port.

- _fqbn_board_id_and_options(): split board id and menu options, keeping
  the old last-segment behaviour for plain 3-part FQBNs.
- _arduino_board_flags(): CDCOnBoot=cdc overlays cdc_on_boot=1 on the
  boards.txt base flags (mirroring <id>.menu.CDCOnBoot.cdc.build.
  cdc_on_boot=1); the per-board cache stores only base flags, overrides
  are applied on a copy.
- _arduino_variant(): board id extraction goes through the same helper so
  a menu suffix no longer corrupts the variant lookup.

First user: the ESP32-C3-LCDkit board def pins
esp32:esp32:esp32c3:CDCOnBoot=cdc — the real kit has no UART bridge, its
only console is the USB Serial/JTAG port, and the esp32c3js engine now
models the always-attached host that HWCDC's connection gate requires
(esp32c3js v1.8.0).
2026-08-19 16:39:59 +02:00
David Montero Crespo 7dc88b45e0 fix(examples): code-only examples honor languageMode in the filename
A micropython example that ships only 'code:' landed its Python in
sketch.ino (the mode switch was right, the name was not - seen on the
SensAirShuttle ws2812 example). main.py / main.c / sketch.ino by mode.
2026-08-19 15:28:47 +02:00
David Montero Crespo 3d2a3534b4 fix(examples): tolerate missing components/wires in an example def
The interface requires both fields but the Docker build never
type-checks the pro overlay's example data (vite build only), so a
missing field shipped and loadExample's unguarded .filter/.map threw -
the page surfaced as a sticky 404 on a registered example (four
SensAirShuttle examples on staging). Load an armless example as having
none instead; authoring still declares the fields required.
2026-08-19 15:02:22 +02:00
David Montero Crespo bf81ac4ae5 feat(compile): esp32p4 defaults to PSRAM enabled
The Function-EV module always carries 32 MB AP-hex PSRAM and esp_lcd
allocates DSI framebuffers from it unconditionally - without it the
board's flagship peripheral cannot run. Explicit psram=disabled still
wins; the UI only sends the field after the user opens the
board-options modal. Validated: an optionless espidf compile now embeds
esp_psram.
2026-08-19 10:47:37 +02:00
David Montero Crespo eb8d86e1aa feat(compile): enable PSRAM for esp32p4 with the P4's hex-mode symbols
The P4 gate ('until the engines validate it') can flip: esp32p4js
v0.3.3 models the AP hex PSRAM + MSPI1 command controller. The classic
SPIRAM chunk emitted S3-era symbols (SPEED_80M, MODE_QUAD/OCT) that do
not exist in the P4's Kconfig.spiram - it gets its own chunk: MODE_HEX
+ SPEED_200M (tree default, rev0-safe; 250M needs rev >= 3). Validated
on staging: an esp_lcd_ek79007 espidf build with psram=enabled boots in
the engine to streaming 1024x600 DSI frames.
2026-08-19 10:44:06 +02:00
David Montero Crespo fdf32eb32a feat(picker): ESP-SensAirShuttle ad card
The ESP32-C5 sensing devkit lands in the hosted editor (BME690 + BMM350
Bosch shuttles, 1.9" touch LCD, WiFi 6). Same contract as the other
Espressif cards: id matches the hosted BoardKind (esp-sensairshuttle,
in LAUNCH_ITEMS) so the ad vanishes where the real board registers and
stays suppressed during the embargo.
2026-08-19 10:10:54 +02:00
David Montero Crespo af3f47826d feat(picker): ESP-VoCat and ESP32-P4-Function-EV ad cards
Two more Espressif boards land in the hosted editor; the OSS picker
advertises them like the ESP-EYE/LCDkit/C6 cards. Ids match the hosted
BoardKinds (esp-vocat, esp32-p4) so the ads vanish automatically where
the real boards register, and both ids are in LAUNCH_ITEMS so the
hosted build suppresses the ads during the embargo.

Also removes two accidental double commas that left holes in the ad
arrays.
2026-08-19 07:57:46 +02:00
David Montero Crespo f606da2cbe feat(compile): resolve registry components from includes in every mode
Generalizes the esp_camera.h lesson into a table (esp_lcd_st77916,
esp_lcd_touch_cst816s, esp_lcd_gc9a01, esp_lcd_ek79007, esp32-camera)
and applies it in the arduino branch, the no-core pure-IDF fallback AND
the explicit language=espidf branch (issue #139) - the last one is where
the VoCat display example lives and previously died at the first
include. Validated on staging: the ST77916 colour-bars espidf example
now fetches espressif/esp_lcd_st77916 from the registry and compiles.
2026-08-19 07:51:10 +02:00
David Montero Crespo 2656376cd5 fix(compile): shield the sync endpoint from client disconnects
nginx 504s a cold ESP-IDF build long before it finishes; Starlette then
cancels the handler and the cancellation killed the build subprocess
MID-WRITE, leaving truncated .obj files in the persistent per-target
build cache - every later build of that target failed with
'ranlib: file truncated' until the cache was wiped (bitten twice on
staging the day the P4 lane landed). asyncio.shield lets the build run
to completion with a consistent cache; only the response is lost, and
the async /start + /status flow remains the right path for long builds.
2026-08-19 07:29:26 +02:00
David Montero Crespo 010a112bc8 feat(compile): ESP32-P4 and ESP32-C5 targets in the espidf lane
_idf_target mapped only c3/c6/s3 and defaulted everything else to
'esp32', so a P4 fqbn compiled with the XTENSA toolchain and
-DARDUINO_ESP32P4_DEV. Adds p4/c5 predicates, IDF5-target membership,
0x2000 bootloader offsets, the riscv toolchain requirement, PSRAM held
off until the engines validate it, P4 CPU-freq drops (its kconfig
choices do not overlap the classic set), a rev0 sdkconfig pin for the P4
(the in-browser engine runs the only published mask ROM and v3 firmware
reads a flash descriptor rev0 never wrote), and an honest refusal for C5
arduino builds (no core exists). Validated on staging: an esp32p4
arduino sketch now produces a merged image with the riscv32 toolchain
that the esp32p4js engine accepts.
2026-08-19 06:35:33 +02:00
David Montero Crespo c4860d4f23 feat(boards): widen esp32Family with p4/c5 - loud failure on the absent QEMU path
The overlay's ESP32-P4 (and the coming C5) run ONLY on their in-browser
engines: neither chip has a QEMU machine anywhere. toQemuBoardType now
throws a clear error for those families instead of booting the wrong
machine and wedging on the first register.
2026-08-19 04:42:24 +02:00
David Montero Crespo 149fbee1e9 feat(boards): ESP-EYE ad card + declarative builtInCamera on ProBoardDef
Overlay boards with a camera now declare builtInCamera on their def (bool,
or { maxFrameBytes } for drivers with tight frame buffers) and the canvas
CameraToggle keys off it - the hardcoded esp32-cam / xiao-esp32s3-sense
kinds keep working unchanged.
2026-08-19 02:50:49 +02:00
David Montero Crespo 9733047a9f feat(boards): ESP32-C3-LCDkit ad card (online-only)
The knob kit joins the overlay board program (Espressif partner batch);
OSS builds show the shop-window card until the pro overlay registers the
real kind.
2026-08-19 02:33:33 +02:00
David Montero Crespo bd1613a9e2 feat(micropython): ship v1.28.0 firmware for the whole ESP32 family
The esp32/s3/c3 MicroPython bins were v1.20.0 (April 2023) — built by
micropython.org on IDF 4.4, three IDF generations behind the compile
pipeline and the engines' own validation corpus (both IDF 5.5), and two
behind the C6 bin (already v1.28). That drift is what broke I2C on the
S3: the engines decode the IDF 5.x driver sequences, and the v1.20 image
drove them the 4.4 way.

All four families now ship v1.28.0 (2026-04-06, IDF 5.x). Remote URLs
and cache keys move together, so a browser holding the old bin in
IndexedDB misses the new key and fetches v1.28 on its own. The new
images all declare 4 MB flash — padToFlashSize's floor exactly (the old
S3 bin declared 8 MB).

Validated cold on each engine (boot -> REPL, i2c.scan finds an attached
slave, WLAN scan/connect/DHCP; S3 additionally quad-PSRAM with a
multi-MB heap) and in the app (scan [60] on esp32/s3/c3, the ssd1306
gallery example drawing). Companion engine release: esp32s3js v1.16.0
(I2C interrupts on both cores, quad-PSRAM data plane). Pin.toggle() and
three years of upstream fixes come along; the v1.20 Pin.toggle note in
the editor seed is now history only.

Residual risk, flagged not fixed: the OSS QEMU self-host path (no JS
engines) now boots v1.28 under QEMU — unvalidated here, velxio.dev does
not exercise it.
2026-08-19 00:57:43 +02:00
David Montero Crespo 4eb8060dcb fix(sensors): illumination sliders are logarithmic, like the quantity they set
The lux slider was linear 0-1000, but an LDR's response (and light
perception) is logarithmic: in the night-light example the LED's whole
on/off behaviour lived below ~20 lux — 2% of the slider's travel — and the
remaining 98% did nothing, reading as "the sensor barely works". The
physics were right (the divider model puts the example's 500-count
threshold at ~20 lux, sensible for a night light); the CONTROL was wrong.

The photoresistor and photodiode lux sliders now run in log position
space: value = min + 10^(p·log10(span+1)) - 1, anchored exactly on min
and max (the +1 shift gives the log axis a zero). Mid-travel lands in the
tens of lux, and the night-light toggles near the middle of the track
instead of at its first pixels. Same mapping on the component's own
on-canvas slider. The model, the examples and the reported lux values are
untouched — only where a given lux sits on the track changed.

Slider controls gain an opt-in `scale: 'log'`; everything else stays
linear.
2026-08-18 22:42:25 +02:00
velxio-deploy 6bd173bba7 chore(examples): refresh 1 thumb file(s) [auto] 2026-08-18 22:31:43 +02:00
David Montero 33ee270ae1 compile: arduino-cli builds no longer queue behind ESP-IDF builds
One Semaphore(2) gated every compile, so while two ESP32 builds ran (cmake +
ninja, minutes on a cold cache) an Arduino Uno blink sat 'compiling' for as
long as they took. Two lanes now: heavy (ESP-IDF, still 2 — the VPS load
constraint is real) and light (arduino-cli boards, 3), plus a console line
when a job is queued so the wait is visible instead of looking like a hung
build. The per-target lock that keeps concurrent builds out of one ESP-IDF
build dir is unchanged.
2026-08-18 22:08:54 +02:00
David Montero Crespo d7a4b5e4eb feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:

Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.

Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.

The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.

Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-18 21:50:10 +02:00
David Montero 9936d1956c flash: compile before flashing, stale-build warning, i18n dialog, entitlement seam (#207)
"Flash to real board" no longer needs a prior Compile. The menu item is
enabled for every Arduino/ESP-IDF board (MicroPython keeps its own
gating) and the Flash dialog compiles first when the board has no program
or its code changed since the last build — compiler output streams into
the dialog's console, a red build ends in an error banner there with a
"Recompile & flash" retry, and only a green build reaches the flasher.
Web Serial's port picker needs a user gesture, so the web flow asks the
overlay to grant the port at the click (new WebFlashImpl.preparePort)
before the compile that may take minutes.

Stale detection: compileBoardProgram() now records a fingerprint of the
sources + language + build options (BoardInstance.compiledSourceHash,
utils/sourceFingerprint.ts); the dialog warns and rebuilds when the live
sources differ. utils/boardCompile.ts holds the compile request builder
shared with the toolbar (same files, same options).

The whole dialog and the menu hints are translated (editor.flash.* and
editor.canvas.flashHint.* in the 9 locales); before, every string was
hard-coded English.

Entitlement seam for #207 (desktop "upload to board"): lib/proWebFlash.ts
gains installHardwareFlashGate(); OSS default allows, and when a gate
blocks, the dialog shows a paid-license panel with a "See plans" link
instead of the port picker.
2026-08-18 20:21:57 +02:00
David Montero Crespo c21bfca220 fix(editor): the workspace panel stacks its actions, and the toolbar stops growing a hole
Two places where the editor bar spent width on nothing.

The file explorer put its label and its buttons on one line: WORKSPACE next
to new/open/save, and every board section's name next to rename / new file /
new folder plus the status dot. Between them those set a floor under the
panel's width, and the board name — the one thing a user reads there — got
whatever was left ("M5 Cardput..." in a 150px pane). Both headers now stack:
the label owns its row, the actions sit above (board) or below (workspace),
and the status dot rides in the actions row, where it costs the name
nothing. The actions row keeps its height reserved whether or not the
buttons are showing, so hovering a section never shoves the files below it
down. Default pane width 150 -> 124px; the freed px go to the code editor.

The toolbar's center slot is a flexible spacer that pins the right icons to
the far right, and nothing renders into it (no caller passes centerSlot). So
whenever the bar had width to spare — closing the docked AI chat is exactly
that — the spacer ate it: 191px of black between Reset and Libraries on a
1600px window. Empty, it no longer grows, so the editor controls stay packed
and the surplus collects at the one boundary that already carries a divider,
in front of the canvas controls (which belong over the canvas pane).
2026-08-18 18:42:38 +02:00
David Montero 13a19c127a prerender: initialise i18n for SSR; example pages mounted under their route
Now that SSR bodies are actually injected, two more prerender defects
showed: every t() rendered as its key (i18next was never initialised in
entry-server — the English bundle is imported now, plus the overlay's
"pro" namespace on pro builds, with an OSS stub), and every example
detail page rendered "404 Example not found" because ExampleDetailPage
was mounted without a Route so useParams() was empty. Verified locally:
/examples/blink-led SSRs breadcrumb, h1, description and JSON-LD; the
gallery and editor summary render translated copy.
2026-08-18 18:35:01 +02:00
David Montero 2c43c638a1 prerender: SSR bodies were never injected; examples gallery renders on the server
replaceRootSeo() swaps the static #root-seo fallback for the page's SSR
body by string search. The old regex required a <script> right after the
block, but Vite hoists the module script into <head> in the built
index.html, so it never matched: every prerendered page (landings, /v3,
/about, all 171 example pages) shipped the HOMEPAGE's fallback body —
same <h1>, same nav — under its own <head>. Two hundred near-duplicate
bodies, and none of the landings' actual content (h1, sections, FAQ)
ever reached the crawler.

ExamplesGallery passes a server snapshot to useSyncExternalStore so
/examples SSRs instead of falling back to meta-only.
2026-08-18 18:10:36 +02:00
David Montero e1fc833f61 seo: starter examples are "New <board> project" pages
data/starters.ts maps eight free boards to the gallery example that opens
the editor with the board and a working blink (blink-led, nano-blink,
mega-blink, esp32-blink-led, esp32s3-blink-led, c3-blink, pico-blink,
attiny85-blink). ExampleEditorPage gives those ids an action-shaped
title/description ("New ESP32 project — free online ESP32 simulator")
while the gallery keeps the sketch title; the prerender writes
dist/starters.json so the prod app shell can serve the same head in the
raw HTML. This is the Wokwi "New ESP32 Project" shape: a URL of its own,
canonical of its own, that lands inside the product with the board loaded
— a candidate for brand sitelinks with usage intent.
2026-08-18 17:38:13 +02:00
David Montero 6f19cf9068 seo: /v2 and /v2-5 canonical to the home and out of the sitemap
Superseded release pages that were taking two brand sitelink slots (5.9k
impressions, 29 clicks). They still render for old links; hreflang is
only emitted for a page that is its own canonical (prerender + useSEO).
2026-08-18 17:29:54 +02:00
David Montero 18b133311e editor: tolerate the trailing slash now that /editor is prerendered; nav links to /editor/
A fresh load of /editor is 301'd to /editor/ by nginx (prerendered dir),
while client-side navigation still lands on /editor. The three exact
pathname comparisons (header active state, header share button, starter
dialog gate) now ignore the trailing slash, and the header nav links to
/editor/ so crawled internal links match the canonical with no redirect
hop.
2026-08-18 17:22:47 +02:00
David Montero 54e25cfed5 seo: seo-routes.json lists every SEO route with metadata, not only the prerendered ones
So the prod app shell can give a route that has metadata but (yet) no
prerendered file its own head, self canonical and hreflang instead of the
homepage's.
2026-08-18 17:21:12 +02:00
David Montero 4da00840c6 seo: own-locale canonical + hreflang for localized pages; /editor prerendered
- useSEO: for the static SEO routes (marketing pages, examples gallery,
  editor, home) the canonical is now the page's OWN locale variant and the
  head lists every locale variant as rel=alternate hreflang plus x-default
  on the locale-less English URL. /es/ and /zh-cn/ were declaring
  canonical=/ while Google indexed and ranked them on their own — an
  unstable contradiction. UGC pages (projects, profiles, example detail
  pages) are the same English content under every prefix and keep a single
  locale-less canonical with no hreflang.
- prerender-seo.mjs emits the same hreflang set into each prerendered page
  and writes dist/seo-routes.json (path/title/description) so the prod app
  shell can serve the /<locale>/<route> variants (which have no static file)
  with the route's own head.
- /editor gets seoMeta and a static SSR summary component, so it is
  prerendered: nginx now serves /editor/ as a real page and 301s /editor to
  it. Both forms answered 200 with the homepage head before, and Google
  indexed /editor and /editor/ as two pages.
2026-08-18 16:50:39 +02:00
David Montero 1b735c9c30 seo/analytics: real GA attribution, tight robots.txt, no build-date lastmod, GA only on velxio.dev
- trackClickCTA sent the originating page as the event parameter `source`,
  a reserved traffic-source name in GA4: every landing CTA click rewrote the
  session's acquisition to landing / arduino-simulator / rpi-simulator with
  medium (not set), which put 16% of sessions in the Unassigned channel.
  Renamed to cta_source (register it as a custom dimension in GA4 if the
  click_cta report should keep breaking down by page).
- robots.txt is one group with no blank lines (some parsers end a group at
  a blank line, orphaning the Disallow rules); /login, /register and /admin
  are no longer disallowed — they carry noindex, and a page must be
  crawlable for that tag to take effect. /project/ and /api/ stay out.
- The static sitemap no longer stamps every URL with the build date as
  <lastmod>; an all-identical, ever-changing lastmod is discarded as
  unreliable, so omitting it is the honest option.
- The GA4 tag loads only on velxio.dev / www.velxio.dev / tauri.localhost:
  self-hosted copies of the app (there are >180 hostnames reporting into
  the property) no longer pollute velxio.dev's analytics.
2026-08-18 16:04:48 +02:00
David Montero Crespo 189ba355a9 feat(boards): a board can ship the code it opens with
The editor seeded a freshly placed board from its FAMILY: the Arduino
LED_BUILTIN blink, the Pico Pin(25) MicroPython blink, or the Raspberry
Pi RPi.GPIO script. None of those run on a board that has no such LED,
no RPi.GPIO and its own vendor library, so the first Run failed on a
board the user had not touched yet.

ProBoardDef gains defaultFiles (per language mode, plus 'python' for the
QEMU-Linux guests) and defaultLibraries; addBoard and setBoardLanguageMode
prefer the board's own seed over the family default, and the seeded
libraries land in the board's compile manifest.

Also fix the MicroPython family fallback: it recognised an ESP32 by
'esp32' appearing in the group id, so an overlay ESP32 whose kind carries
no such substring (m5stack-core, cardputer-adv) was seeded the Pico's
Pin(25) blink - a dead pin on both. It now consults the board's declared
esp32Family too.
2026-08-18 15:02:13 +02:00
velxio-deploy 0a0badd10f chore(examples): refresh 2 thumb file(s) [auto] 2026-08-18 09:18:51 +02:00
velxio-deploy 3b213e9e02 chore(examples): refresh 20 thumb file(s) [auto] 2026-08-18 09:01:45 +02:00
David Montero b9f663f02e fix(editor): the toolbar strip's label collapse is measured too, and each layout is judged by its own widths
Follow-up to 534b2ff. Dropping the strip to its own bar was now measured,
but two things still produced a wrapped bar on the XIAO example at 1440
with the chat docked:

- The first collapse tier (Serial / Camera / Mic / Scope / Add / Libraries
  labels -> icons) was still a fixed 1010px container query, so on the
  1060px own-bar the labels stayed and the bar wrapped in two. That tier is
  now the unified-toolbar--compact class, set by the same measurement:
  labels on the brand row if they fit, icons on the brand row if those fit,
  else the own bar — labelled if the full bar holds it, icons otherwise.
  The finer tiers (board pill 905, count/zoom 835, view toggle 760/1140)
  stay as container queries.
- Each candidate layout is measured IN that layout: the zones carry their
  own container queries (the view-mode toggle hides on a narrow row and
  reappears on the wide bar), so a width read on the brand row said the
  bar would fit when it would not.
2026-08-18 05:49:45 +02:00
David Montero 534b2ff21b fix(editor): the header measures whether the toolbar strip fits instead of guessing by breakpoint
With the AI chat docked at 1440px, a board with many canvas controls (XIAO:
display + camera + mic + scope) wrapped the strip INSIDE the header — the
canvas controls orphaned on a second line next to a black hole — because
the drop-to-own-bar fallback fired on media-query thresholds (1040 / 1060 /
1400px by chat state) that were only right for one board's strip width.

AppHeader now measures: natural single-row width of the strip (each zone at
its content width) against the room left next to the brand + menus, and
sets app-header--strip-below when it does not fit. Always measured in the
inline layout (class removed, measured, restored before paint) so the
below-state's margins and uncollapsed labels never bias the decision, and
there is nothing to oscillate. Re-measured via ResizeObserver on the strip
host, the brand block and the zones (plus a MutationObserver for the
portaled canvas controls), deferred a frame so a toggle never re-enters
delivery. The three media-query blocks collapse into one class rule; a
1000px window without the chat now keeps one row when the collapsed strip
fits, instead of being forced to two.
2026-08-18 05:21:21 +02:00
David Montero 7fc131d4fb feat(editor): compact file explorer — less indent, smaller icons and rows, 150px default
The explorer rows carried a 26px indent plus 22px file icons, 5px vertical
padding and 12px names in a 165px pane: a lot of margin for a sidebar
whose only job is listing a handful of files, and every px of it was taken
from the code editor. Now: root files indent 16px (12px per nested level),
file icons render at 16px like the folder icon, rows are 3px/6px with an
11px name, the board header actions are 18px, and the header buttons 20px
with a title that truncates before the actions do. The pane defaults to
150px (min 100) and the account footer keeps the first name visible at that
width.
2026-08-18 04:54:49 +02:00
David Montero e71c78f734 fix(canvas): boards added from the picker land in the visible corner like components
Boards were placed 420 world-px right of the ACTIVE board plus 60/30 px per
board already on the canvas: a fixed world offset that ignored pan/zoom (the
new board could land off-screen) and kept marching further out with every
board added, even after the earlier ones had been moved away or removed.
Components had already been fixed to start at the visible top-left and take
the first free slot (utils/dropSlot); boards now go through the same rule,
and both count each other as parked items so they cascade over the same
slots.
2026-08-18 04:39:24 +02:00
David Montero 990cced209 feat(editor): the Save button is the auto-save indicator; drop the header pill
The header rendered a "Saved" / "Unsaved changes" text pill next to the
menus whenever a project was loaded. It ate width in the one row that has
to hold the whole editor toolbar, and it duplicated a signal the Save
button in the workspace panel can carry by colour alone.

The pill is gone. FileExplorer takes the AutoSaveState and colours its Save
button: green saved, orange unsaved changes, pulsing while saving, red on
failure; grey (unchanged) with no project loaded. The tooltip carries what
the pill used to say plus the last-save time. Nine locales get the four new
strings under editor.fileExplorer with English fallbacks.

AppHeader no longer takes autoSave; EditorPage passes it to FileExplorer
instead. No pro overlay code referenced the pill or the prop.
2026-08-18 02:26:35 +02:00
David Montero Crespo a0aeb101cd test: declare `canvas` so the jsdom tests get a real 2D context
captureDisplay.test.ts runs under @vitest-environment jsdom and says in its own
header that "jsdom + the `canvas` package give a real 2D context here" — but
canvas was never a direct devDependency. jsdom lists it as optional, so npm
skipped it, getContext('2d') returned null, and five tests died on
`ctx.fillStyle` with a TypeError. That aborted the pre-deploy test gate.

Declaring it directly makes the install deterministic; the lockfile already
carried the entry. All six tests pass with it present.
2026-08-18 02:05:01 +02:00
David Montero Crespo 20fc27e680 fix(assets): Pi 4/5 photos had an opaque white background
On the landing's dark board tiles they rendered as a white slab around the
board. Keyed the near-white out to transparent and cropped to the board:
Pi 4 goes 1024x1024 -> 922x599. Pi 5 kept its framing (it had no margin) and
just gained the alpha.
2026-08-18 01:35:39 +02:00
David Montero Crespo 9fb887dd13 i18n(landing): hero board count 19 -> 35, matching the boards section
Same page was advertising two different numbers after the boards section grew.
2026-08-18 01:01:32 +02:00