Commit Graph

864 Commits

Author SHA1 Message Date
davidmonterocrespo24 1ab294cf10 feat(sim): Phase 1b continued, step 1 — scheduler voltage cache + subscriber routing
Adds the runtime plumbing that Phase 1b's SPICE event loop will drive:
- `publishVoltage(componentId, pin, voltage)` updates a (componentId,
  pin) → volts cache and notifies every matching subscriber.
- `getCurrentVoltage(...)` reads the cache (was previously stubbed
  null).
- subscribe/publish routing exercised by 7 new unit tests.

The scheduler still does not yet drive ngspice — `start()`,
`onMcuPinChange()` are unchanged. But once Phase 1b's solve loop is in
place, calling `publishVoltage` after each `readVec` is all the wiring
needed for components to start reacting to SPICE-resolved analog
states. This is the smallest non-trivial step that keeps the
architecture honest (no test-only emitters; the same code path will be
used in production).

Tests skip booting the WASM worker — they call publishVoltage
directly, so they pass in plain Vitest with no JSDOM Worker shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:06:20 +02:00
davidmonterocrespo24 d9945d13b8 feat(sim): Phase 2.1 — migrate MOSFETs to VDMOS macro-models
Switches 3 of the 4 simulated MOSFETs from Level=1 Shichman-Hodges
to LTSpice VDMOS macro-models. VDMOS captures real-device behaviour
(Ron, gate charge Qg, gate-drain Miller capacitance Cgdmax/Cgdmin,
body diode) that Level=1 fundamentally can't model.

Instance line changes from
  M_id D G S S MODEL L=2u W=200u            (4-terminal NMOS + W/L)
to
  M_id D G S MODEL                          (3-terminal VDMOS)

Parts migrated:
  mosfet-2n7000  → 2N7002 VDMOS (Vto=1.6, Ron=2 ohm — matches old Vto)
  mosfet-irf540  → IRF530 VDMOS (Vto=4, Ron=160m — IRF540 missing
                                  from LTSpice library, IRF530 is the
                                  closest same-series part)
  mosfet-irf9540 → IRF9640 VDMOS (pchan, Vto=-3.5 — IRF9540 missing,
                                  IRF9640 is the 200V P-channel sub)

mosfet-fqp27p06 kept on Level=1 (no upstream VDMOS equivalent yet).

spice-mosfet-pwm regression test still passes: Id=8.6 mA at Vgs=5V,
0 at Vgs=0V, monotonic across the ramp. All 155 SPICE + analog
examples + lockdown tests pass.

Phase 2.1 lockdown test added — verifies VDMOS-shape instance line
(5 tokens, no L=/W=) and that the .model card carries `VDMOS(`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:03:49 +02:00
davidmonterocrespo24 7508423c6e test(sim): Phase 2 lockdown — guard BJT/diode model upgrades
Asserts the Phase 2 BJTs include Gummel-Poon junction caps (CJC/CJE)
and forward transit time (TF), and the diode upgrades include
reverse-recovery time (tt) and Schottky band-gap (Eg). If anyone
simplifies the models in the future, these regress fail and surface
the loss of AC/transient fidelity.

Also guards the dedupe identity between the canonical diode-1n4148
emission and the relay flyback diode — they must serialise as the same
string or ngspice will reject the netlist for duplicate .model lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:59:39 +02:00
davidmonterocrespo24 c476084e00 feat(sim): Phase 2 — upgrade BJT/diode models to LTSpice Gummel-Poon (SPICE3F5)
Replaces the truncated 4-5 param NPN/PNP/D models in componentToSpice.ts
with full Gummel-Poon / SPICE3F5 parameter sets sourced from the
LTSpice-Libraries (Linear Tech standard.bjt and standard.dio). Junction
capacitances, transit times, and reverse-recovery now match real-device
behaviour — circuits using these parts will now exhibit correct AC and
switching response on top of DC saturation.

Parts upgraded:
  BJT NPN: 2N2222, BC547, 2N3055
  BJT PNP: 2N3906, BC557
  Diode:   1N4148 (silicon switching), 1N5817, 1N5819 (Schottky)

MOSFET (Level=1) and 1N4007/zener kept as-is - they need separate
VDMOS migration validated against the MOSFET PWM regression test.

Phase 2.0 of the mixed-mode simulator project. See
velxio-prod/project/sim-mixedmode/phase-02-device-models.md.

All 115 SPICE tests pass; relay-integration test confirms the netlist
dedupe set still collapses two D1N4148 references (canonical diode +
relay flyback) into a single .model line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:57:36 +02:00
davidmonterocrespo24 cb07a88095 feat(sim): Phase 3 — logic families (TTL/CMOS-5V/LVCMOS33/AVR_HC/Schmitt)
Replaces the Phase 1b vcc/2-flat threshold with per-logic-family
Vil/Vih thresholds + Schmitt-trigger hysteresis where applicable.
SPICE-resolved digital reads now match what real ICs actually do —
TTL noise margins, CMOS rail-to-rail, 74HC14 Schmitt hysteresis,
LVCMOS33 vs CMOS-5V interop.

New module: simulation/LogicFamilies.ts
  - LogicFamily interface (vcc, vil, vih, vil_schmitt?, vih_schmitt?,
    cin_pF, vol_max?, voh_min?, output_impedance_ohm?)
  - FAMILIES catalog: TTL, CMOS-5V, CMOS-5V-SCHMITT, CMOS-5V-TTL-INPUTS,
    LVCMOS33, AVR_HC, CMOS-3.3V — all sourced from TI / ATmega328P /
    JEDEC datasheets.
  - BOARD_FAMILY: per-board lookup. Uno/Mega/Nano/ATtiny → AVR_HC,
    ESP32 family + Pi Pico → LVCMOS33, fall back to AVR_HC for
    unknown boards.
  - getBoardLogicFamily() and getLogicFamilyById() helpers.

PinResolver:
  - SpiceResolvedConfig docstring rewritten with Phase 3 wording.
  - New `configFromLogicFamily()` builder — picks Schmitt thresholds
    when the family declares them, falls back to vih/vil otherwise.

DynamicComponent:
  - When the trace crosses an active device, the SPICE-resolved
    resolver is now built with the OWNER BOARD's logic family
    instead of vcc/2. Hysteresis comes through automatically for
    boards whose native family is Schmitt-capable.
  - Phase 3 continued: per-component logicFamily override from
    components-metadata.json (so e.g. a 74HC14 placed on an Arduino
    Uno gets Schmitt thresholds even though the BOARD is AVR_HC).

Tests:
  - logic-families.test.ts (new) — 19/19 passing.
    Covers catalog sanity (vil < vih, vol_max ≤ vil, voh_min ≥ vih),
    per-board lookup, Schmitt vs non-Schmitt config, noise rejection
    behavior of 74HC14 Schmitt resolver, last-state-wins behavior
    of CMOS-5V dead band.
  - Phase 0 + Phase 1b regression: 16/16 still passing.
  - tsc --noEmit on new files: clean.

No deploy in this commit — staged for end-of-session rebuild.
2026-05-15 16:42:11 +02:00
davidmonterocrespo24 27c59664cd feat(sim): Phase 1b skeleton — SPICE-resolved PinResolver + active-path detection
Adds the architecture pieces for mixed-mode coupling without yet
driving the SPICE engine.  Components on a path that crosses an active
device (BJT, MOSFET, op-amp, diode, regulator, LED, relay) now route
through a new SPICE-resolved PinResolver variant; everything else
keeps the digital fast-path from Phase 0.

What ships:

  - simulation/PinResolver.ts
    * `isActiveDevice(metadataId)` predicate + `ACTIVE_DEVICE_PREFIXES`
      list (BJTs, MOSFETs, op-amps, diodes, regulators, LED, relay).
    * `DetailedPinTrace` / `DetailedPinTracer` types — the trace
      function now reports whether it crossed an active device, on
      top of the Arduino pin number.
    * `createSpiceResolvedPinResolver()` — new factory; reads voltages
      from a `SpiceVoltageSource` and threshold-converts to HIGH/LOW
      with hysteresis (thresholdHigh != thresholdLow → Schmitt-like).

  - simulation/spice/MixedModeScheduler.ts (new)
    * Singleton orchestrator that holds the NgSpiceInteractive engine
      and the SpiceVoltageSource subscription registry.
    * `start()` / `stop()` / `dispose()` lifecycle.
    * `subscribe()` + `getCurrentVoltage()` implement SpiceVoltageSource.
    * `onMcuPinChange()` placeholder for the alter+tran event loop.
    * Skeleton: subscribers register but never receive events yet.
      Phase 1b continued will wire NgSpiceInteractive into the loop.

  - components/DynamicComponent.tsx
    * Trace function extended with `traceDetailed()` that tracks
      whether the BFS crossed an active component.
    * PinResolver factory branches: active-path → SPICE-resolved (uses
      the scheduler), digital-only → existing default impl.  Default
      threshold = vcc/2 with no hysteresis; Phase 3 will replace with
      per-logic-family Vil/Vih.

Phase 0 LED behavior intact (digital path).  Phase 1b SPICE-resolved
path falls back to FLOATING until Phase 1b continued wires the engine.

Tests:
  - pin-resolver-phase1b.test.ts (new) — 8/8 passing.
    Covers isActiveDevice for every BJT/MOSFET/op-amp/diode/regulator
    metadata id; SPICE-resolved resolver state reporting, threshold
    conversion, hysteresis dead-band, unsubscribe.
  - pin-resolver.test.ts (Phase 0) — 8/8 still passing (no regression).
  - tsc --noEmit on the new files: clean.

No deploy in this commit — staged for end-of-session rebuild + push
per user preference.
2026-05-15 16:38:30 +02:00
davidmonterocrespo24 d41be4f48b feat(sim/spice): vendor ngspice+XSpice WASM and add NgSpiceInteractive client (Phase 1a)
Phase 1a of the mixed-mode simulator project.  Vendors prebuilt
ngspice+XSpice WASM artifacts from ejkreboot/ngspice-xspice-wasm (MIT,
2026) and adds a TypeScript client that exposes the ngspice shared
callable API for interactive (event-driven) use.

What's vendored at frontend/public/wasm/ngspice-interactive/ (~27 MB):

  - ngspice-lib.wasm  (24 MB) — ngspice 33 + XSpice, MAIN_MODULE
  - ngspice-lib.js   (2.7 MB) — Emscripten glue
  - {analog,digital,xtradev,xtraevt,table,tlines,spice2poly}.cm
                              — XSpice code models, loaded dynamically
  - spinit                    — ngspice startup script
  - PROVENANCE.md             — sources + license info

Note on the cost: 27 MB is a one-way commit to git history, but the
existing eecircuit-engine dependency already ships 39 MB in node_modules
(not tracked, re-downloaded per build). Vendoring our copy:
  - removes a third-party npm dependency
  - pins the exact build we tested with
  - means the WASM is served as a static asset (no Vite chunking)
The alternative (publish as @velxio/ngspice-interactive-wasm) was
deferred to keep the iteration cycle fast during Phase 1+.

New TypeScript client at frontend/src/simulation/spice/wasm/:

  - NgSpiceInteractive.ts        — Promise-based client class with
                                    init / loadNetlist / command /
                                    alter / readVec / reset / dispose
  - ngspice-interactive-worker.js — vendored from ejkreboot's worker
                                    and extended with 'loadNetlist',
                                    'command', 'readVec' message types
                                    plus per-command stdout/stderr
                                    capture

POC test at __tests__/ngspice-interactive.test.ts (skipped in node env
because Worker isn't available; runs in a browser-mode test env):
  - voltage divider .op → reads v(mid) ≈ 2.5V
  - RC step → reads v(cap) time series, final ≈ 5V
  - alter Vsrc → second .tran → final ≈ 1V (proves alter+rerun works)

Known limitation deferred to Phase 1b: the vendored WASM is built
without pthreads (no -sUSE_PTHREADS=1), so ngspice's bg_run is
synchronous-blocking. True mixed-mode event injection requires a
pthread-enabled rebuild (with SharedArrayBuffer + cross-origin
isolation). For Phase 1a we use the workaround: chained short-tran
invocations with `alter` between them. The new architecture is built
to swap in a real bg_halt/bg_resume implementation later without
changing component handlers — see NgSpiceInteractive.ts docstring.

Tests passing:
  - pin-resolver (Phase 0): 8/8
  - ngspice-interactive: 3 skipped (need browser env)
  - tsc --noEmit on the new files: clean
2026-05-15 16:14:50 +02:00
davidmonterocrespo24 e10492c6e6 feat(sim): introduce PinResolver abstraction (Phase 0 of mixed-mode rewrite)
Decouple per-component handlers from direct pinManager.onPinChange +
getArduinoPinHelper subscriptions by introducing a small PinResolver
interface. The Phase 0 default impl is functionally identical to the
legacy path — it just routes through PinResolver instead of being
inlined in every handler. Zero behavior change.

The point is to make Phase 1 possible: swap the default impl for a
SPICE-resolved version that watches node voltages and threshold-
converts to digital events, without rewriting every handler.

Files:
  - simulation/PinResolver.ts (new) — interface + default factory
  - parts/PartSimulationRegistry.ts — additive 5th arg to
    attachEvents (getPinResolver?), legacy 4-arg signatures keep
    working unchanged
  - components/DynamicComponent.tsx — assembles the PinResolver from
    the wire-trace logic + PinManager subscriptions + board Vcc
    lookup, passes it as the 5th arg to attachEvents
  - parts/BasicParts.ts — LED handler migrated as proof of concept
    (resolver-first path, legacy 4-arg path kept as fallback for
    tests / unmigrated harnesses)
  - __tests__/pin-resolver.test.ts (new) — 8 unit tests covering
    FLOATING / GND / HIGH / LOW / GPIO subscriptions / unsubscribe

Vitest: 8/8 pin-resolver tests pass. 1300+ existing tests still pass;
the one pre-existing flake (spice-rectifier-live-repro timing out >60s)
is unrelated to this commit — verified by running the test on plain
HEAD without these changes (same timeout).

See project/sim-mixedmode/phase-00-pin-resolver.md (in the velxio-prod
repo) for full phase context.
2026-05-15 15:50:09 +02:00
David Montero Crespo a91b857d8c fix(ili9341): rotation 3 was mirrored — use explicit per-rotation map
The MADCTL handler in 6edc715 applied MX/MY/MV as three independent
flags, then mirrored physX/physY post-swap. That double-applies the
mirror for setRotation(3) (which Adafruit sends as MX|MY|MV|BGR=0xE8):
expected formula for rotation 3 is

  physX = 239 - curY
  physY = curX

but the flag-by-flag approach computed

  physX = 239 - curY      (correct by coincidence)
  physY = 319 - curX      (mirrored — should be just curX)

so every landscape-rot-3 sketch rendered horizontally flipped. The
user's Pico Doom title screen looked mirrored even after the previous
fix landed.

Replaced with an explicit per-rotation table derived from
Adafruit_ILI9341's setRotation() source:

  rot 0  MX|BGR        : (curX, curY)
  rot 1  MV|BGR        : (curY, 319 - curX)
  rot 2  MY|BGR        : (239 - curX, 319 - curY)
  rot 3  MX|MY|MV|BGR  : (239 - curY, curX)

Selects the case based on (madMV, madMX, madMY) bits, which is
straightforward because Adafruit only emits these 4 specific values.
Other drivers that set arbitrary MADCTL combinations (e.g. with the ML
or MH bits) still fall through to the closest of the four — good
enough for the screens we actually run.

Build verified (vite OSS+pro, 285 SEO pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:32:37 -03:00
davidmonterocrespo24 d79f2923d9 fix(sim): trace through BJT C↔B in getArduinoPinHelper
The canonical "Arduino pin → resistor → BJT base, BJT collector →
load" pattern for multiplexed 7-segment clocks was breaking in the
simulator: getArduinoPinHelper('COM.1') couldn't resolve through
the transistor, so the multiplex-aware 7-segment driver thought no
digit-select pin was wired and fell back to "all digits enabled".
Result: every display in the multiplex array rendered the same
rapidly-changing pattern → user-visible flicker.

Fix: add the NPN/PNP BJTs to the PASSIVE_PIN_PAIRS map with
[collector, base] — the trace function continues from B when it
arrives at C (and vice versa). That makes the Arduino pin driving
the base reported as the controller of the collector — exactly the
relationship the user's multiplex code expects.

Conventions covered:
  - NPN (2n2222, bc547, 2n3055): Arduino HIGH → transistor on →
    COM pulled LOW → common-cathode digit enabled.  Our 7-segment
    driver treats "digit pin HIGH = enabled" which matches.
  - PNP (2n3906, bc557): inverse logic.  We expose the same pin
    mapping; users writing PNP-driver code will see the polarity
    behave inverted, which is what real hardware does too.

This is a one-line shortcut, not a true active-device model. We're
not simulating BJT saturation, β, base current, or PNP polarity —
just reporting "this Arduino pin is the boss of this collector".
That's enough for the multiplexing use case and the only place
getArduinoPinHelper is consulted today.
2026-05-15 06:32:26 +02:00
davidmonterocrespo24 76e0d77975 fix(sim/7segment): multiplex-aware driver — track COM/DIG pins, latch segments per digit
The simulator's 7-segment part used to write segments straight into
element.values[0..7] regardless of how many digits the display has and
without considering the COM/DIG select pins.  That meant:

  - Multi-digit displays (digits=2/3/4) only ever lit digit 0; the
    other digits stayed dark even when their DIGn pin was driven.
  - For 1-digit displays multiplexed via shared A-G bus + per-display
    COM.1 transistor (the canonical Arduino clock pattern), all four
    displays showed the same rapidly-changing segment pattern and
    rendered as flickering gibberish because COM.1/COM.2 were ignored.

This rewrites the part:

  - Per-element state: live segments[] (Arduino-driven A..DP), per-
    digit latched digitValues[][], and digitEnabled[] flags.
  - Subscribes to the right digit-select pins for the digit count
    (COM.1/COM.2 for digits=1, DIG1..DIGn for digits=2/3/4).
  - On segment-pin change: writes to segments[] AND mirrors into
    every currently-enabled digit's latched slot.
  - On digit-pin LOW->HIGH (= enable, transistor-driver convention):
    latches the live segments[] into that digit's slot so the first
    refresh after enabling reflects the current pattern.
  - When NO digit-select pin is wired to an Arduino pin (pure direct
    drive, COM tied to GND): all digits default to enabled so segment
    writes propagate immediately — preserves the old behaviour for
    the simplest single-digit case.
  - Rebuilds element.values as a flat array of length digits*8 (the
    shape wokwi-7segment-element expects: indices d*8..d*8+7 = digit
    d's A..DP).

Result: multiplexed 4-digit clocks built with 4 separate 1-digit
7segments + transistors actually render the four digits as the user
intended.  Direct-drive single-digit displays still work unchanged.
2026-05-15 06:08:38 +02:00
David Montero Crespo 1e4d78fda5 fix(board): raspberry-pi-pico renders a real Pico, not Nano RP2040 Connect
The 'raspberry-pi-pico' boardKind used to render <NanoRP2040> — a
<wokwi-nano-rp2040-connect> Web Component. That's a completely
different board: it has pin labels D2..D13 / A0..A7 / 5V / VIN,
and a horizontal 168×68 layout. The actual Raspberry Pi Pico has
GP0..GP28 / 3V3 / VBUS / VSYS and is vertical-narrow (105×264).

Symptom: every wire in a Pi-Pico example that referenced a real Pico
pin (GP10, GP18, 3V3, GND.5, etc.) silently fell back to (0, 0) in
pinPositionCalculator — the calculator looks up `element.pinInfo`
by name, doesn't find GP* on the Nano RP2040 Connect component, and
returns the board's top-left corner. The Pico Doom example was the
loudest casualty (cables to the corner instead of the TFT), but
seven other GP-style examples (pico-7segment, pico-button-led,
pico-rgb, pico-dht22, pico-doom-raycaster, plus pico-ntc/pico-joystick
which use A0/A1 aliases that map to GP26/GP27) all silently routed
to nowhere.

Fix is a two-liner: 'raspberry-pi-pico' shares the same case as
'pi-pico-w' (both use the same Web Component because the Pico and
Pico W are pin-compatible). BOARD_SIZE updated to 105×264 to match
the real Pico footprint. Dropped the now-unused NanoRP2040 import.

Known regression — eleven older examples (pico-blink, pico-serial-led-
control, pico-i2c-scanner, pico-i2c-rtc-read, pico-i2c-eeprom-rw,
pico-spi-loopback, pico-adc-read, pico-multi-protocol, pico-hcsr04,
pico-pir, pico-servo) were wired against D2..D12 of the wrong board.
Their wires will now land at (0,0). Those examples' sketches were
written for the Pi Pico (use LED_BUILTIN = GP25, A0..A3 = GP26..GP29)
so the wires were ALREADY electrically nonsense — they connected
external components to pins the sketch never touched. Visible bug
trades silent bug; both need a follow-up commit to rewire each one
to the Pico pin its sketch actually expects.

Combined with the earlier MADCTL fix (6edc715) and the SPI adapter
fix (6a7b721), Pico Doom should now finally render end-to-end on
velxio.dev.

Build verified (vite OSS+pro, 285 SEO pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:31:19 -03:00
David Montero Crespo 6a7b72138f fix(rp2040): route SPI0 through the adapter in initMCU too
Long-standing latent bug: SPI parts (ILI9341, custom chips, etc.)
register a handler on simulator.spi.onByte via the lazy adapter, but
the actual rp2040.spi[0].onTransmit assignment in initMCU was a pure
loopback that never consulted the adapter. The MicroPython init path
(initMicroPython) had the adapter-aware version since day one;
the Arduino path (initMCU) didn't.

Symptom: Pico Doom + every other Arduino sketch driving an ILI9341
on the RP2040 saw an empty SPI bus. The ILI9341 emulator's onByte
handler was wired up correctly — it just never received a single
byte. Pantalla negra.

Fix: copy the adapter-aware handler from initMicroPython (line 219)
into initMCU (line 441). Each byte the firmware writes to SPI0 now
checks `_spiAdapter.onByte` first; if a part is registered, it gets
the byte; otherwise we keep the original loopback as the fallback
so plain "echo MOSI back as MISO" sketches still work.

Combined with the earlier MADCTL fix (commit 6edc715) and the
power+MISO wiring fix (8440836), Pico Doom should now render its
title screen + the raycaster.

Build verified (vite OSS+pro, 285 SEO pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:23:36 -03:00
David Montero Crespo 65cbc403d2 feat(examples): /example/<id> route with pinned URL
Mirror of the /project/<uuid> pattern but for built-in examples.
Loading an example used to navigate to a generic /editor and lose
all trace of which example was loaded — same URL whether you
clicked Blink or Doom, nothing shareable, no back-button history.

New page: pages/ExampleEditorPage.tsx
  - Route: /example/:exampleId  (singular, distinct from the plural
    /examples/<id> landing).
  - useEffect calls loadExample(...) once when exampleId changes,
    guarded by a ref so React strict-mode's double-effect doesn't
    re-load (which would clobber any edits the user made).
  - Renders <EditorPage /> after the load completes — same as how
    ProjectByIdPage stays mounted at /project/<uuid> after load.
  - SEO: title + description per example, canonical URL points at
    /example/<id>.
  - 404 state for unknown ids (typo'd link, deleted example).
  - Inline install progress while libraries fetch — the overlay
    UI moved here from ExamplesPage/ExampleDetailPage so progress
    is visible right at the URL you'll bookmark.

App.tsx — registered the new route alongside the existing landing.
Both coexist on purpose:
  /examples/<id>  = SEO landing page (preview, badges, "Open in
                    Simulator" CTA). Indexed by Google (130 URLs
                    already in sitemap.xml).
  /example/<id>   = live editor with the example pre-loaded; URL
                    stays pinned so the link is shareable +
                    bookmarkable like a saved project URL.

ExamplesPage — gallery now navigates to /example/<id> instead of
calling loadExample directly. Also drops the install-overlay block
(progress UI is on ExampleEditorPage now).

ExampleDetailPage — "Open in Simulator" navigates to /example/<id>
instead of loading directly. Drops its own install overlay too.

Side effect: this also kills the data-loss bug from 95f2aa9 in a
second way. Even if a future change forgets to call
clearCurrentProject() somewhere, navigating into ExampleEditorPage
forces a fresh page transition — the previous project's state +
the auto-save subscription don't survive into the example session.

Build verified (vite OSS+pro, 285 SEO pages prerendered).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:14:36 -03:00
David Montero Crespo 95f2aa9a9f fix(loadExample): clear currentProject before mutating stores
Critical data-loss bug. Repro:

  1. User opens a saved project at /<username>/<slug>. The page sets
     useProjectStore.currentProject = { id, slug, ownerUsername, ... }.
     Auto-save kicks in and starts watching simulator/editor stores.
  2. User clicks the "Examples" link, picks an example, hits Run.
  3. loadExample mutates useSimulatorStore (setComponents, setWires,
     addBoard, removeBoard) and useEditorStore (loadFiles).
  4. Auto-save sees the change. Its eligibility check finds
     currentProject still pointing at the user's saved project (we
     never touched useProjectStore). It debounces a
     PUT /api/projects/<old-id> with the EXAMPLE's components/wires/
     files. The user's saved project is overwritten with the example
     contents.

The URL changing to /editor isn't enough — useProjectStore is store
state, not router state. ProjectPage / ProjectByIdPage set it on
mount; nothing clears it when the user navigates away.

Fix: loadExample calls useProjectStore.getState().clearCurrentProject()
BEFORE the simulator/editor mutations. autoSaveImpl is subscribed to
useProjectStore via subscribe((s, prev) => ... reset() if id changed),
and Zustand notifies subscribers synchronously inside set(), so the
reset (projectId=null, baseline hash=null) runs in the same tick.
Every subsequent setComponents/setWires/loadFiles fires onChange in
the hook, which now sees projectId=null and returns early. No PUT
ever goes out.

The reset is order-sensitive: it must run BEFORE the store mutations
or the hook would already have queued a save with the old projectId
before we cleared. Comment in the source spells this out so it
doesn't get reordered in a future refactor.

In-flight saves are not affected: buildSavePayload() snapshots state
before its `await updateProject(...)`, so a save that started right
before the example load still sends the user's pre-example state to
the right project. Worst case: the save completes after clear, and
the hook quietly returns idle.

Build verified (vite OSS+pro, 285 SEO pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:02:05 -03:00
David Montero Crespo 844083657c fix(examples/pico-doom): wire ILI9341 power + MISO
The Pico Doom example loaded with the ILI9341 dangling on three
critical pins:

  - VCC  — unconnected (no 3V3 from the Pico)
  - GND  — unconnected (no return path)
  - MISO — unconnected

On real hardware the TFT wouldn't power on at all without VCC/GND.
Inside the velxio simulator the missing power isn't strictly fatal
(the simulator drives pixels off the SPI bus, not the rail), but it
makes the schematic incorrect and misleading for users who copy it
to a breadboard. MISO is electrically idle for write-only drivers,
but Adafruit_ILI9341 with the 3-arg constructor binds to hardware
SPI0, so MISO physically maps to GP16 — leaving it floating leaves
the SPI bus topology incomplete.

Wires added:

  Pico 3V3   → tft1.VCC   (red)
  Pico GND.5 → tft1.GND   (black) — closest GND pad to GP17/18/19
  Pico GP16  → tft1.MISO  (amber) — hardware SPI0 MISO

Updated the data-integrity test (examples-pico-doom.test.ts) to
include the three new pairs in the SPI/control/power expectation
map. 10/10 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:22:31 -03:00
David Montero Crespo 6edc715e8d fix(ili9341): handle MADCTL so landscape (setRotation 1/3) renders
The ILI9341 emulator hardcoded SCREEN_W=240 SCREEN_H=320 and silently
ignored every command except CASET/PASET/RAMWR/SWRESET. The block
comment even bragged about it ("All others are silently accepted —
init sequences, DISPON, MADCTL…").

That's fine for portrait sketches, but every landscape demo —
including the new Pico Doom raycaster — calls tft.setRotation(1) or
setRotation(3). Adafruit_ILI9341 translates those into MADCTL 0x36
with the MV (row/column exchange) bit set, then issues CASET windows
with X∈[0..319] and PASET windows with Y∈[0..239]. The emulator's
bounds check `curX > colEnd` would let curX reach 319, but the
buffer write `id.data[(curY*240 + curX)*4]` would land in a slot
that belongs to a different row — and worse, the SCREEN_W=240
ceiling silently truncated everything past column 239. Net result:
black screen for any rotated sketch.

Fix: parse MADCTL (0x36) and treat CASET/PASET as LOGICAL coordinates.
At pixel-write time, remap (curX, curY) → physical (px, py) using the
MV/MX/MY bits, then write into the still-physical 240×320 imageData.
SWRESET resets MADCTL back to portrait defaults (matches the
datasheet's reset semantics).

  MADCTL bit  Mask    Meaning
  D7 MY       0x80    row mirror
  D6 MX       0x40    column mirror
  D5 MV       0x20    swap X/Y (landscape)

Verified by rebuilding (vite OSS+pro). The fix is data-flow only —
no API change, no new dependency. Pico Doom should now actually
render its title screen + raycast frames in /examples on the
raspberry-pi-pico board.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:19:03 -03:00
davidmonterocrespo24 9ace6b7476 fix(store): addBoard promotes itself to active when none is valid
addBoard appended the new board to the boards[] array but never
touched activeBoardId. The default INITIAL_BOARD_ID points at a
board the picker injects on first load — but a fresh anonymous
session (or a project that landed in a state without that initial
board) can have activeBoardId pointing at nothing.

When the agent does add_board('arduino-uno') → compile_sketch, step
2 then fails with "no active board on the canvas" and the model
burns a turn on set_active_board.

The fix: at addBoard time, if activeBoardId doesn't resolve to any
existing board, promote the new board to active. If there IS a
valid active board, leave it alone — manual placements of additional
boards via the picker still keep focus on whatever the user was
working on.
2026-05-15 03:38:34 +02:00
davidmonterocrespo24 f0953fcf45 i18n: admin.users keys for "Reset agent usage" button
New keys across all 9 locales (en/es/fr/de/it/pt-br/ja/ru/zh-cn):
  - admin.users.actions.resetAgentUsage           — button label
  - admin.users.actions.resetAgentUsageTooltip    — hover hint
  - admin.users.confirmResetAgentUsage            — confirm dialog
  - admin.users.resetAgentUsageDone               — success toast
  - admin.users.resetAgentUsageFailed             — error toast

Consumed by the velxio-prod overlay's AdminPage Users tab, which adds
a "Reset agent" button per row that hits
POST /api/admin/users/{user_id}/reset-agent-usage and clears today's
pro_agent_usage_events for the user.  Live agent quota recomputes
from the events table, so the user can keep using the agent
immediately after the button click.
2026-05-14 23:29:21 +02:00
davidmonterocrespo24 6242b7f16b fix(minimap): hit-test against clamped rect + shrink to 100x75
Two unrelated minimap issues from user feedback:

1. Click on the red viewport rect was sometimes teleporting the
   canvas instead of starting a drag.  Cause: insideRect compared
   click coords against the UNCLAMPED rectX/rectY/rectW/rectH, but
   the rendered rect uses clampedX/clampedY (which differ when the
   user pans past a world edge).  The user clicked on the visible
   red rect, but the logical rect was off-minimap → insideRect
   returned false → fell through to the teleport branch.

   Fix: compute clamped values once at the top, render and hit-test
   against the same values.  Drag now only fires when the click
   really lands inside the visible rect.

2. The 140x105 default still ate too much canvas at typical zoom.
   Drop to 100x75 (12% of world width by 2.5%, same proportions as
   the world).  Mobile breakpoint dropped to 90x68 to stay
   proportionally smaller on phones.
2026-05-14 22:53:34 +02:00
davidmonterocrespo24 218a891c6d feat(minimap): shrink to 140x105 + red viewport rect
User feedback: the default 200x150 minimap eats too much of the
canvas-content area on a typical 13"/14" laptop, and the white
viewport rectangle against a dark canvas blends with the boards
once enough components are placed.

Drop the desktop default down to the size we already use on phones
(140x105 — the mobile media query still wins on screens ≤720px so
that block continues to apply identically). At this size the rect
becomes the focal indicator of where you are in the world; switch
its outline to brand red (#ef4444 — Tailwind red-500) with a faint
red fill so it pops without overpowering the boards (which stay
brand blue).

Body of the work is two number changes + two color tokens; the
rest of the component logic (pointer routing, world rendering,
clamping) is untouched.
2026-05-14 22:32:42 +02:00
David Montero Crespo 28d9cbc490 chore(oss): drop dead auth/DB dependencies from OSS image
After Phase 4 of the OSS / pro split, the OSS code base imports zero
auth/DB modules (verified with grep across backend/app/). But the
requirements.txt + config.py + .env.example + docs still listed
SQLAlchemy, aiosqlite, JWT/bcrypt, OAuth, SECRET_KEY etc. as if they
were live. Self-hosters running `pip install -r requirements.txt`
were pulling ~30 MB of packages the code never imports.

Changes:

* backend/requirements.txt — drop sqlalchemy, greenlet, aiosqlite,
  python-jose, passlib[bcrypt], bcrypt, authlib, email-validator,
  python-multipart. Keep fastapi, uvicorn, websockets, pydantic,
  pydantic-settings, httpx, mcp, esptool, wasmtime — everything OSS
  actually uses.
* backend/app/core/config.py — Settings reduced to FRONTEND_URL only.
  Comment explains the overlay path that adds the rest at Docker
  build time.
* backend/.env.example — same trim: only FRONTEND_URL, with a comment
  explaining why this file is almost empty.
* README.md — "Auth & Project Persistence" section rewritten to
  describe .vlx export/import. Env-var table reduced to a single row.
  Stack table updated: no SQLAlchemy, no JWT, persistence = .vlx
  files.
* CLAUDE.md — intro line updated (Auth: None, persistence: .vlx).
  Key-file-locations rewritten to list the OSS-stateless backend +
  the new lib/proRoutes / proSession / proSaveAction seams, with an
  explicit "removed in the split" note pointing to velxio-prod.
  Stores section drops useAuthStore (overlay-only now). Backend
  gotchas drop the bcrypt + email-validator + model-import notes.
  Implemented-features list replaces "Auth + URL persistence + user
  profile" with portable .vlx export/import.
* docs/ESP32_EMULATION.md — two `docker run` examples dropped the
  `-e SECRET_KEY=...` arg (no longer needed).

OSS build verified end-to-end (285 SEO pages prerender, 20 stateless
routes, zero sqlalchemy imports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:06:27 -03:00
David Montero Crespo b4ab742456 feat(oss): portable .vlx project export/import for self-hosters
Phase 4 of the OSS / pro split. The OSS image has no auth and no
server-side persistence — without this commit, the user's workspace
was ephemeral (lost on tab refresh). `.vlx` is a single-file JSON
snapshot of the entire workspace (boards, file groups, components,
wires, active board id) that the user can save to disk and reload
later.

New: utils/vlxFile.ts
  - buildVlxPayload() / buildVlxBlob() — pure snapshot of the current
    editor + simulator stores.
  - triggerDownloadVlx({ name? }) — anchor-click download with a safe
    filename. Returns the filename actually used.
  - parseVlxFile(File) — async reader + validator. Checks
    format === "velxio-project", version <= 1, and the required
    arrays/objects are present. Throws VlxParseError with a human-
    readable message on any issue.
  - importVlxFile(File) — convenience wrapper that parses AND calls
    useSimulatorStore.loadProjectState() with the result.

Format intentionally mirrors the server's POST /api/projects body so
a Pro user can export-from-pro and import-into-OSS losslessly (and
vice-versa once Pro adds an Export button — out of scope here).

lib/proSaveAction.ts: the default (no-overlay) implementation now
calls triggerDownloadVlx() instead of console.info'ing about the
missing handler. The Pro overlay still wins via installSaveActionImpl()
— Save in Pro keeps opening SaveProjectModal. The Save button in OSS
now actually saves.

components/editor/FileExplorer.tsx: new "Open .vlx" button next to
New + Save. Opens a hidden file input; confirms with the user before
replacing the workspace (loadProjectState is destructive); surfaces
VlxParseError messages via window.alert.

Verified with both builds:
  - OSS-only: triggerSaveAction → download .vlx; FileExplorer shows
    3 buttons (New, Open, Save).
  - OSS + overlay: Pro's installSaveActionImpl overrides — Save opens
    SaveProjectModal as before. Open .vlx still works (independent
    button, not part of the save flow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:33:00 -03:00
David Montero Crespo 7a3996776b
Merge pull request #186 from davidmonterocrespo24/hotfix/compile-422-hooks-request-annotation
fix(hooks): annotate get_current_user_id(request: Request)
2026-05-14 15:50:10 -03:00
davidmonterocrespo24 1190cc1c35 fix(hooks): annotate get_current_user_id(request: Request)
Without the type annotation, FastAPI treats `request` as a Query
parameter and bubbles it up to every endpoint that uses
`Depends(get_current_user_id)`. Result: POST /api/compile/start
and POST /api/compile/ both returned 422
{"loc":["query","request"],"msg":"Field required"} on every call
the frontend made — compile was fully broken in production.

The frontend then caught the 422 axios error and surfaced
response.data as a CompileResult, which had no success/stdout/
stderr/error fields, so the editor's CompilationConsole rendered
only the fallback "✕ Compilation failed" line with no detail.

Annotating `request: Request` is the standard FastAPI pattern;
the framework injects the raw HTTPRequest and no longer treats
it as a query parameter.
2026-05-14 20:47:35 +02:00
davidmonterocrespo24 1fb7518226 fix(hooks): annotate get_current_user_id(request: Request)
Without the type annotation, FastAPI treats `request` as a Query
parameter and bubbles it up to every endpoint that uses
`Depends(get_current_user_id)`. Result: POST /api/compile/start
and POST /api/compile/ both returned 422
{"loc":["query","request"],"msg":"Field required"} on every call
the frontend made — compile was fully broken in production.

The frontend then caught the 422 axios error and surfaced
response.data as a CompileResult, which had no success/stdout/
stderr/error fields, so the editor's CompilationConsole rendered
only the fallback "✕ Compilation failed" line with no detail.

Annotating `request: Request` is the standard FastAPI pattern;
the framework injects the raw HTTPRequest and no longer treats
it as a query parameter.
2026-05-14 20:44:57 +02:00
David Montero Crespo 5c993d6c2a refactor(oss-split): remove auth/admin/profile frontend from OSS
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.

What moved to the private overlay (pro/frontend/src/pro/):
  pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
  pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
  components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
  components/layout/{SaveProjectModal,LoginPromptModal}.tsx
  services/{authService,adminService}.ts
  store/useAuthStore.ts
  hooks/autoSaveImpl.ts

New seams added so OSS components stay decoupled:
  * lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
    useSyncExternalStore. mountPro() injects the moved pages at runtime;
    App.tsx subscribes to the registry, so registration after the
    initial render re-renders without a Not-Found flash.
  * lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
    App.tsx fires this on mount instead of useAuthStore.checkSession();
    pure OSS no-ops.
  * lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
    EditorPage's Save button dispatches through this; the overlay
    decides whether to show SaveProjectModal or LoginPromptModal based
    on auth state. In OSS without an overlay it's a no-op today; in
    Phase 4 of the split it becomes the .vlx Export entry point.

OSS-side rewrites:
  * App.tsx drops the 8 page imports + 8 route entries; uses
    triggerSessionCheck() instead of useAuthStore directly.
  * AppHeader.tsx drops the user/login/register block entirely. The
    header-auth slot (introduced in Phase 1) now stays empty in OSS
    and gets filled by the overlay's portal mount.
  * EditorPage.tsx drops useAuthStore + SaveProjectModal +
    LoginPromptModal imports. The Save handler is now triggerSaveAction().
  * LandingPage.tsx drops the dead UserMenu component (defined but
    never rendered) + its useAuthStore imports.
  * main.tsx drops the side-effect import of hooks/autoSaveImpl — the
    impl lives in pro now and self-registers via mountPro().

Build config:
  * vite.config.ts adds @velxio alias → src/. Lets the overlay import
    upstream modules (lib/proRoutes etc.) by stable name regardless of
    whether it's symlinked (local dev) or COPYed (Docker).
  * preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
    mode). Needed so Rollup keeps the overlay logically inside src/pro/
    during local junction-based builds.

Build verification:
  * OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
    pages prerendered. Bundle drops ~80-120 KB.
  * OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
    HeaderAuth dropdown injected via slot, save action wired to the
    overlay's modal flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:31:12 -03:00
David Montero Crespo 908a160003 refactor(oss-split): remove auth/DB/admin stack from OSS
Phase 2 of the OSS / pro split. The hook seams introduced in Phase 1
let stateless routes (compile, libraries, simulation, iot_gateway)
run without the auth/DB stack importable. Now we actually delete the
stack:

  app/api/routes/auth.py
  app/api/routes/projects.py
  app/api/routes/admin.py
  app/api/routes/metrics.py
  app/models/{user,project,usage_event,password_reset_token}.py
  app/schemas/{auth,admin,project}.py
  app/core/{dependencies,security}.py
  app/database/session.py
  app/services/{metrics,odoo_mail,project_files}.py
  app/utils/{geo,slug,boards}.py

Private deployments (velxio.dev) get the same modules back via the
velxio-prod overlay: pro/backend/app/api/routes/auth.py etc. are
COPYed onto /app/... at container build time, and register_pro()
includes their routers + registers the lifespan/metrics/auth hooks.

main.py shrank back to the stateless router includes + a single
`run_lifespan_startup()` call. The Phase-1 try-import block that wired
record_compile / get_current_user_id from upstream is gone — those
adapters live in pro now.

Verification:
  OSS only:     20 routes (compile, libraries, simulation, gateway).
  OSS + pro:    94 routes — identical to pre-refactor velxio.dev.

Net change: -2400 lines from OSS, all of which moved to velxio-prod's
overlay. Self-hosted OSS users lose accounts + project persistence;
the Phase 4 .vlx export/import gives them a portable replacement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:36:31 -03:00
David Montero Crespo 12b6e94e4d refactor(oss-split): introduce extension hooks for auth, DB, metrics, auto-save
First phase of the OSS / pro split. Goal: open the seams so the auth/DB/admin
stack can move into the private overlay (Phase 2-3) without the routes that
stay in OSS (compile, libraries, simulation, iot_gateway) having to know.

Backend
-------
* New app/core/hooks.py — registry for record_compile, get_current_user_id,
  and lifespan startup tasks. Each hook is a no-op by default; overlays
  call register_* in register_pro(app) to plug in a real implementation.
* compile.py now imports only from app.core.hooks. Drops the direct deps on
  app.core.dependencies, app.database.session, app.models.user, and
  app.services.metrics. Route signatures use `Depends(get_current_user_id)`
  instead of `Depends(get_current_user)`; the metric helper passes user_id
  through rather than a User instance.
* compile_chip.py drops the unused _current_user Depends entirely.
* main.py wraps the auth/DB stack import in try/except. When it succeeds
  (today's behavior on velxio.dev), an adapter bridges record_compile and
  get_current_user_id to the existing app.services.metrics + dependencies,
  and the create_all + ALTER TABLE migration block runs via a registered
  lifespan_startup hook. When it fails (the post-Phase-2 OSS image), main
  logs "running stateless" and skips registering anything — the routes
  still load and behave as no-ops for metrics + always-anonymous for auth.

Frontend
--------
* useAutoSaveProject becomes a skeleton: one useState + one useEffect that
  delegates to an installed AutoSaveImpl. installAutoSaveImpl() replaces
  the impl without changing hook count, so React's rules-of-hooks stay
  satisfied even after the impl moves out of OSS.
* New hooks/autoSaveImpl.ts holds the original logic (debouncing, dirty
  detection, owner eligibility, fetch keepalive on unload), refactored to
  emit() instead of useState. It self-registers at module load; main.tsx
  imports it for the side effect.
* AppHeader wraps the entire user-vs-login UI in a data-velxio-slot
  ="header-auth" boundary. Today the OSS UI still renders inside the slot
  — the overlay can portal-inject additional items now, and in Phase 3
  the slot becomes the sole owner of header auth UX.

Behavior is identical on velxio.dev (pro overlay imports everything
successfully, every adapter wires up). The change is purely structural:
deleting the auth/DB modules tomorrow no longer crashes OSS at import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:24:51 -03:00
David Montero Crespo a9200da631 fix(seo): drop #root-seo on mount so it stops inflating page scroll
index.html ships a #root-seo div with prerendered SEO content (so crawlers
that don't run JS still index per-route copy). The inline CSS comment said
"React removes it on mount", but nothing actually removed it — so every
page kept a position:absolute, ~4096px-tall, visibility:hidden element
parked at top:0. That element does not paint, but it DOES contribute to
document.documentElement.scrollHeight.

Symptom: /admin, /docs, /:username and other short pages had a phantom
scroll roughly the size of the prerendered SEO body. Scrolling past the
real content showed a black band (just the body background) because there
was nothing visible to render down there. When tab content loaded with
more rows, the real content outgrew the phantom and the scrollbar "settled
in" — matching the user-reported symptom exactly.

Verified with puppeteer against velxio.dev:
  /dave:  documentElement.scrollHeight 4096 → expected ~800 after fix
  /admin: documentElement.scrollHeight 4096 → expected ~800 after fix
  /docs:  documentElement.scrollHeight 4096 → expected ~1161 after fix

The removal runs inside App's mount-effect, so it only fires after React
has actually committed — if App were to throw during render, the SEO
fallback would stay in the DOM as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:52:12 -03:00
David Montero Crespo 530174f31e fix(espidf): enable mbedTLS PSK so ssl_client.cpp links
arduino-esp32 v2.0.17's libraries/WiFiClientSecure/src/ssl_client.cpp:23
wraps its entire body in:

  #if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) \
   && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED)
  #  warning "Please call idf.py menuconfig ..."
  #else
    ssl_init / start_ssl_client / stop_ssl_socket /
    send_ssl_data / get_ssl_receive / data_to_read
  #endif

Our esp-idf-template/sdkconfig.defaults did not enable any PSK key-exchange
mode, so MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED was never auto-set by
mbedtls and ssl_client.cpp compiled to an empty translation unit. The
companion WiFiClientSecure.cpp still compiled and ended up in
libarduino-esp32.a with dangling references, breaking the link of every
sketch that pulls in HTTPClient or WiFiClientSecure (directly or
transitively).

Reproduced against the user's WiFi + HTTPClient example.com sketch on the
prod server and again locally with ESP-IDF v4.4.7 + arduino-esp32 v2.0.17;
the prebuilt sdkconfig that ships with arduino-esp32 itself sets both
flags, so we just align with that.

After the fix the same sketch links cleanly:
  velxio-sketch.bin binary size 0xbf470 bytes ... 25% free

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:38:20 -03:00
David Montero Crespo e1ac29b3c5
Merge pull request #185 from davidmonterocrespo24/feat/compile-logs-store-slot
feat(compile): expose compile logs via Zustand store + UI slot
2026-05-14 11:51:39 -03:00
davidmonterocrespo24 729c8785ba feat(compile): expose compile logs via Zustand store + UI slot
Two minimal hooks so the velxio-pro agent overlay can offer a 'Diagnose
this compile failure with AI' affordance without touching upstream
component internals:

  - New store/useCompileLogsStore: holds the editor's compile output as
    Zustand state instead of local React useState in EditorPage. The
    setter accepts both a value and an updater fn so the EditorToolbar
    callers that used setCompileLogs(prev => [...prev, log]) keep
    working without changes.

  - CompilationConsole header now renders a
    <div data-velxio-slot='compile-console-actions' /> when errorCount
    > 0. The pro overlay mounts a 'Diagnose with AI' button into this
    slot via slotMounter. Empty in the OSS image — no behaviour change.

EditorPage replaces its local useState<CompilationLog[]> with the store
selector. The downstream prop-drilled setCompileLogs callers (toolbar,
sub-toolbars) keep their signature.

Companion commit lands the button + diagnostic prompt builder in the
velxio-prod overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:44:05 +02:00
David Montero Crespo 30004bb82b
Merge pull request #184 from davidmonterocrespo24/feat/bump-quota-limits
feat(landing): bump pricing display to 100/500/2000 daily credits
2026-05-14 03:13:43 -03:00
davidmonterocrespo24 aaebfedd23 feat(landing): bump pricing display to 100/500/2000 daily credits
Mirrors the velxio-prod backend quota bump (PLANS dict in
pro/backend/app/pro/services/quota.py). With 9router serving the bulk
of agent traffic for free, the cost-of-LLM ceiling is much lower than
when the limits were originally tuned, so we can be significantly more
generous and let casual users actually evaluate the agent.

  Free     20  /day, 300 /mo   →  100  /day, 1500 /mo
  Pro      400 /day, 12k /mo   →  500  /day, 15k  /mo
  Pro Max  1000/day, 30k /mo   →  2000 /day, 60k  /mo

Updates the landing.pricing.tiers.{free|pro|pro_max}.f1 string in all
9 locales (de, en, es, fr, it, ja, pt-br, ru, zh-cn) with each
locale's native thousands separator (',' / '.' / ' ' depending on
convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:10:25 +02:00
David Montero Crespo ec690a93f1
Merge pull request #183 from davidmonterocrespo24/fix/publish-docker-license-key
fix(ci): pass VELXIO_LICENSE_KEY to publish workflow
2026-05-14 01:27:41 -03:00
davidmonterocrespo24 4dbb860237 fix(ci): pass VELXIO_LICENSE_KEY to publish workflow
After the Dockerfile.standalone refactor (PR #182), the qemu-provider
stage requires VELXIO_LICENSE_KEY to fetch libqemu .so + ESP32 ROM
blobs from velxio.dev's gated download endpoint. The Publish Docker
Image workflow was missing the build-arg, so it failed on every push
to master and the GHCR / Docker Hub :master image went stale.

Plumbs the existing repo secret VELXIO_BUILD_LICENSE_KEY into
docker/build-push-action@v6's build-args list. Same secret the
backend-e2e-tests workflow already consumes — single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:26:29 +02:00
David Montero Crespo 029f782a8f
Merge pull request #182 from davidmonterocrespo24/feat/docker-fetch-binaries-from-velxio
feat(build): fetch QEMU binaries from velxio.dev license endpoint
2026-05-14 00:57:33 -03:00
davidmonterocrespo24 36bd49f507 feat(build): fetch QEMU binaries from velxio.dev license endpoint
The qemu-prebuilt GitHub Release was the convenience-binary hosting
path before we shipped the license module. With the license module
live at /api/pro/license/downloads/, the prebuilts now live there
behind a free personal-tier key.

Dockerfile.standalone:
  - New build-args VELXIO_LICENSE_KEY + VELXIO_BINARY_BASE_URL
  - prebuilt/qemu/ local files still win first (lets users compile
    QEMU from source per docs/BUILD-QEMU.md and use that instead)
  - Legacy QEMU_RELEASE_URL kept as escape hatch for private mirrors
  - Fail-fast with a friendly message if no path is configured

backend-e2e-tests workflow:
  - Reads secrets.VELXIO_BUILD_LICENSE_KEY (set in repo settings)
  - Uses the gated URL pattern; same fallback message on missing key

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 05:55:38 +02:00
David Montero Crespo 1dd1d37696
Merge pull request #181 from davidmonterocrespo24/feat/docs-build-qemu-from-source
docs: BUILD-QEMU.md + 'Build QEMU from source' docs section
2026-05-14 00:46:13 -03:00
davidmonterocrespo24 888ce03cc3 docs: BUILD-QEMU.md + 'Build QEMU from source' docs section
Adds a transparent self-hosting path for users who would rather not
run Velxio's prebuilt libqemu binaries. The prebuilts have always
been a convenience under the AGPLv3 license; this just documents
how to skip them.

- docs/BUILD-QEMU.md as the canonical step-by-step (dependencies per
  Debian/Arch/macOS, ESP32 xtensa + ESP32-C3 riscv32 configure-and-
  ninja, drop-in instructions, troubleshooting, license notes on the
  QEMU/Velxio GPL-vs-AGPL boundary).
- DocsPage gets a new 'build-qemu' section between Setup and Roadmap
  in the sidebar. Content is hardcoded English (technical reference,
  not marketing copy) and ends with a link to the .md on GitHub.
- nav + SEO meta keys added to all 9 locales (de en es fr it ja
  pt-br ru zh-cn). Body remains English in every locale; technical
  content doesn't need translation for the audience that follows it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 05:44:10 +02:00
David Montero Crespo 3ba20e9d92
Merge pull request #180 from davidmonterocrespo24/feat/header-pricing-link
feat(header): add Pricing link to top nav + landing footer
2026-05-14 00:29:47 -03:00
davidmonterocrespo24 fedb197be0 feat(header): add Pricing link to top nav + landing footer
The /pricing page exists (PricingPlaceholder upstream, real PricingPage
portal-mounted by the private overlay) but had no entry in the top nav.
Adds 'pricing' to header.nav in all 9 locales (de, en, es, fr, it, ja,
pt-br, ru, zh-cn), wires the Link in AppHeader between About and Blog,
and mirrors the link in the landing-page footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 05:28:59 +02:00
David Montero Crespo 623a471623
Merge pull request #179 from davidmonterocrespo24/feat-pico-doom-example
feat(examples): Pico Doom — Wolf3D-style raycaster on RP2040 + ILI9341
2026-05-13 22:43:05 -03:00
davidmonterocrespo24 5742ed0146 feat(examples): Pico Doom — Wolf3D-style raycaster on RP2040 + ILI9341
A new entry in the games category for the Raspberry Pi Pico. Renders
a first-person 3D corridor à la Wolfenstein / early Doom using a
column-wise DDA raycaster — 160 rays per frame drawn straight to a
320×240 ILI9341 TFT via drawFastVLine, no framebuffer. 16×16 tile map
with 5 wall palettes (slate, blood, brown, toxic green, bronze door),
darkened on NS faces so corners read in 3D. Forward / back move,
two more buttons turn the player. 40-px HUD bar.

Why a demo, not the canonical id Software Doom: Graham Sanderson's
rp2040-doom port shoehorns DOOM1.WAD into 2 MB of flash with custom
compression and pushes video out over PIO-driven DVI / VGA — none of
that survives the rp2040js emulator (no PIO accuracy, no flash
mapping for huge assets). A raycaster reproduces the *visual* of
early Doom using only ~67 KB of flash and 9 KB of RAM, which the
emulator runs perfectly.

Pre-flight: arduino-cli compile against rp2040:rp2040:rpipico
already verified inside the Velxio backend container — 3 % flash,
3 % RAM. The Adafruit_GFX + Adafruit_ILI9341 libs the example
declares are already in the gallery's auto-install list.

Bundled:
  test/pico_doom_demo/arduino_sketch.ino — source of truth sketch
  test/pico_doom_demo/README.md          — what + why + pin map
  test/pico_doom_demo/compile_check.sh   — operator script that
    runs arduino-cli against the same FQBN the prod backend uses
  frontend/src/data/examples.ts          — gallery entry (boardType
    raspberry-pi-pico, category games, difficulty advanced, 5
    components, 14 wires)
  frontend/src/__tests__/examples-pico-doom.test.ts — 10 vitest
    assertions: example is registered exactly once, target board /
    category / difficulty match, libraries declared, all four
    pushbuttons present, every wire endpoint references a real
    component id, SPI pin mapping matches the sketch's #define
    block, every button has a GND wire, the renderFrame loop is
    still present in the embedded code.
2026-05-13 22:54:35 +02:00
David Montero Crespo a4893a0da7
Merge pull request #178 from davidmonterocrespo24/feat-canvas-minimap
feat(canvas): minimap with draggable viewport in the bottom-right corner
2026-05-13 17:36:29 -03:00
davidmonterocrespo24 dcf98fd0e5 feat(canvas): minimap with draggable viewport in the bottom-right corner
Renders a 200×150 px overview of the whole 4000×3000 world in the
bottom-right corner of .canvas-content. Boards show as filled blue
rectangles, components as small white dots, and the current viewport
appears as an outlined rectangle the user can drag to pan.

Geometry mirrors the canvas's existing pan+zoom model:
  SCALE_X = MINIMAP_W / WORLD_W = 0.05
  rect.x = -pan.x / zoom * SCALE_X
  rect.w =  viewport.width / zoom * SCALE_X

Two interaction modes, decided at pointerdown by hit-testing the
rectangle:
  - Inside the rect  → drag-pan: keep updating pan as the pointer
    moves, with delta in minimap-px converted back to world units
    by (delta / SCALE) * zoom.
  - Outside the rect → teleport: re-center the viewport on the
    clicked world point.

Pan is clamped so the viewport rectangle never escapes the minimap
bounds (matches the canvas's implicit world boundaries at 4000×3000).
ResizeObserver on the canvas-content keeps the rect accurate when
the user toggles side panels or resizes the window.

Mobile: at ≤720 px width the minimap shrinks to 140×105 px so it
doesn't eat too much of the canvas. Touch events go through the same
pointerdown / pointermove path — no separate touch code path needed
thanks to Pointer Events.

Bundles with: matching CSS file, import + JSX hookup inside
.canvas-content's render tree.
2026-05-13 22:29:46 +02:00
David Montero Crespo 83f11f839a
Merge pull request #177 from davidmonterocrespo24/feat-pricing-multipliers-and-canvas-tone
feat(landing+theme): multiplier pricing copy + softer canvas
2026-05-13 16:06:47 -03:00
davidmonterocrespo24 cd4050c499 feat(landing+theme): multiplier pricing copy + softer canvas
Two small fixes that compound:

1. Drop LLM model names from the landing AI section. The agent
   auto-routes between several providers (9router combo, direct
   DeepSeek, direct Gemini, future-others) and naming any of them on
   the homepage misleads visitors. Replace "DeepSeek-V4-Flash and
   Gemini 2.5 under the hood" with "frontier LLMs auto-routed for
   cost and reliability" so the marketing line stays accurate as the
   provider mix changes.

2. Pricing copy switches from absolute message counts (300/day,
   700/day — small, intimidating, hard to anchor) to comparative
   multipliers (Pro = 20×, Pro Max = 50×). Visitors instinctively
   read these as "much more" without needing to count usage. The
   multipliers reflect the new backend quotas (400/day, 1000/day,
   committed separately in velxio-prod's pro overlay).

3. --color-bg-canvas moves from gray-1000 (#000000) to gray-950
   (#0a0a0c). Pure black collided with the slightly-lighter card
   surface (gray-900 #141416) and produced a harsh transition wherever
   a `min-height: 100vh` page wrapper grew taller than its content —
   visible on docs and user-profile pages with sparse content. The
   2-luminance-step shift removes the jarring while keeping the dark
   palette feel intact. gray-1000 stays in the scale for intentional
   black uses.

All 9 locales updated for (1) and (2).
2026-05-13 21:02:48 +02:00
David Montero Crespo edc7bfa132
Merge pull request #176 from davidmonterocrespo24/feat-landing-ai-agent-and-pricing
feat(landing): AI agent + pricing sections
2026-05-13 15:31:31 -03:00