Commit Graph

193 Commits

Author SHA1 Message Date
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
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
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
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 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 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 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
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
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
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
davidmonterocrespo24 d193954c2f feat(canvas): drag-threshold lets users move parts while running
Closes the long-standing "components are frozen during simulation"
complaint. Once the user clicked Run, interactive wokwi parts
(pushbuttons, slide-switches, potentiometers …) called
stopPropagation in their bubble-phase mousedown handlers and the
canvas's React onMouseDown never fired — so dragging them to
rearrange the layout was impossible without first stopping the sim.

Two surgical changes:

1. DynamicComponent.tsx switches the wrapper from `onMouseDown` to
   `onMouseDownCapture`. Capture phase runs before the inner
   wokwi-element, so the canvas sees the mousedown regardless of
   stopPropagation downstream. The existing posDiff < 5 check in
   mouseup keeps disambiguating click vs drag: a click still falls
   through to the wokwi-element's own mousedown/up for button-press
   semantics, only sustained movement promotes to a drag.

2. SimulatorCanvas.tsx's touch path used to early-return on touchstart
   when interactionRunning + .web-component-container, killing any
   chance of a touch-drag. Now we remember the touch's start position
   in pendingTouchDragRef and let the browser keep synthesizing mouse
   events for the wokwi-element. If the finger drifts past
   DRAG_PROMOTE_THRESHOLD_PX (8 px) onTouchMove cancels the
   passthrough and starts a real component drag — dispatching a
   synthesized mouseup on the original target so the wokwi-element
   doesn't stay visually pressed mid-drag.
2026-05-13 16:51:52 +02:00
David Montero Crespo 083e0df732 fix(canvas): board-less SPICE switches toggle on click instead of opening property dialog
In digital / analog board-less examples the user clicks a slide-switch
or pushbutton expecting it to flip its state. Until this commit the
component property dialog opened instead and the click never reached
the wokwi-element underneath, so:

  - The user couldn't change switch state through the canvas at all.
  - With no state change the SPICE solver kept the old netlist, and
    every downstream LED stayed dark — the symptom that read as
    "voltages change but no LED lights".

Root cause was the gating: SimulatorCanvas only suppressed the
property dialog when `useSimulatorStore.running` was true, but that
flag is bound to an MCU's start/stop. Board-less circuits have no MCU
to start so `running` is permanently false, even when the SPICE engine
has been live since the example loaded.

New derived flag `interactionRunning = running || (boards.length === 0
&& !electricalPaused)` — true whenever the user is in an "interactive"
session, MCU or SPICE-only. Used in three click-handling paths:

  - SimulatorCanvas mouse-up handler: dialog is suppressed and the
    click falls through to the wokwi-element (line 1395).
  - SimulatorCanvas touch-start passthrough: same for touch (line 474).
  - SimulatorCanvas touch-end short-tap: same for tap (line 774).

Also propagated to DynamicComponent so the cursor becomes pointer (not
move) for interactive parts in board-less mode — visual cue that the
user can click instead of just drag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:34:58 -03:00
David Montero Crespo 71616e580d Add end-to-end tests for ESP32 I2C functionality and circuit verification
- Implemented `i2c-esp32-real-firmware.test.ts` to test ESP32 I2C communication via backend and WebSocket.
- Created `load-example-transitions.test.ts` to ensure proper loading of examples between board-less and board-based contexts.
- Added `CircuitVerificationModal.tsx` to display circuit verification results before running simulations.
- Developed `circuitVerifier.ts` to perform pre-flight checks for circuit safety, identifying potential issues like short circuits and component overloads.
- Introduced minimal ESP32 I2C master sketch `esp32_i2c_writer.ino` for testing I2C transactions.
2026-05-12 16:55:15 -03:00
David Montero Crespo a097601a73 Add HD44780Decoder and various I2C sketches
- Implement HD44780Decoder for decoding I2C commands to HD44780-compatible LCDs.
- Add bmp280_bridge_reader.ino to read BMP280 chip_id and status registers via I2C.
- Create i2c_scanner_multi.ino to scan I2C addresses and report responding devices.
- Introduce lcd_i2c_hello.ino to demonstrate basic LCD functionality with I2C.
- Implement pcf8574_bidirectional.ino to test bidirectional communication with PCF8574.
- Add pico_i2c_master_reader.ino for reading BMP280 from a Raspberry Pi Pico.
- Create rtc_lcd_clock.ino to display time from a DS1307 RTC on an I2C LCD.
2026-05-12 14:26:33 -03:00
David Montero Crespo dec7a6ec56
Merge pull request #167 from naweiss/fix/wire-boxes
Fix wire hovering in desktop mode
2026-05-11 12:23:05 -03:00
David Montero Crespo 8de51da5a5 feat(simulator): wire color palette from PR #170 + Copilot suggestions
- Add color picker button to SelectionActionBar for wire selections
- Toggle palette using WIRE_KEY_COLORS swatches
- Pass currentColor and onColorChange from SimulatorCanvas
- Reset showPalette on kind/onColorChange change (Copilot suggestion)
- Use t('editor.selectionBar.changeColor') for title/aria-label (Copilot suggestion)
- Add changeColor i18n key to all 9 locale files

Co-authored-by: naweiss <naweiss@users.noreply.github.com>
2026-05-11 12:14:36 -03:00
naweiss fff98915f5 Fix wire hovering in desktop mode 2026-05-11 08:41:51 +03:00
davidmonterocrespo24 7edb0a6499 fix(editor): add missing useTranslation import in SensorControlPanel
Block 9 added `const { t } = useTranslation()` at line 50 but forgot the
matching `import { useTranslation } from 'react-i18next'`. The component
then crashes the moment a user clicks a sensor on the canvas with
`Uncaught ReferenceError: useTranslation is not defined`, taking the
whole simulator render tree down.
2026-05-10 00:16:03 +02:00
davidmonterocrespo24 4a42a3e9a2 feat(compile): stream live ESP-IDF cmake + ninja output to the console
A user reported on Discord: "the Velxio Console doesn't update anything,
it just waits until the very end and displays everything in one go".
True for the async compile path — /compile/status only carried `state`
and the final `result`, so the editor's CompilationConsole stayed empty
during the 5-7 minute cold ESP-IDF builds and dumped 1500 lines at once
when the build finished.

This wires live build output through the whole stack.

Backend (espidf_compiler.py)
- New _run_with_streaming() helper. When a progress_callback is provided
  it spawns the subprocess via Popen + stdout/stderr drain threads and
  invokes the callback line-by-line. When None it falls back to the
  existing subprocess.run(capture_output=True) one-shot path so the
  unit-test code that doesn't care about live output is unaffected.
- compile() and _compile_in_dir() take an optional ProgressCallback.
- _run_cmake / _run_ninja closures now go through _run_with_streaming
  with that callback. cmake configure (~2-5 s) + ninja (~5-300+ s) both
  stream now; the ninja output is the one users actually want to watch.

Backend (compile.py)
- _compile_job seeds COMPILE_JOBS[id]['stdout_buffer'] = '' and defines
  on_progress_line(line) which appends to it. Buffer capped at 256 KB
  (tail kept) so a runaway build can't OOM the FastAPI process.
- The buffer is preserved on both the success and the error path so
  late polls still see the log even after state transitions to
  done/error.
- /compile/status now returns the buffer as a `stdout` field.
  CompileStatusResponse gains the field with default '' so old clients
  that don't read it still work.

Frontend (compilation.ts)
- compileCode() takes a 4th argument: optional CompileProgress
  callback fired every poll while state ∈ {pending, running}. Carries
  the cumulative stdout (caller computes deltas) plus elapsed seconds.
- Surfaces the new `stdout` field of /compile/status and forwards it
  to the callback. Errors thrown from the callback are swallowed —
  a faulty UI hook must never break the polling loop.

Frontend (EditorToolbar.tsx)
- Both compileCode() call sites (Run and Compile-All) now pass an
  onProgress callback. It tracks `lastStreamedLen` per-compile, splits
  each new delta on newlines, and appends them as `info`-typed
  CompilationLog entries via setCompileLogs. The Compile-All flow
  prefixes each line with the board label so multi-board builds stay
  readable.
- After the build settles, the existing parseCompileResult call still
  runs and appends the structured analysis on top of the live stream
  — that's where FAILED-block detection + the `error`-typed entries
  that drive the auto-switch-to-errors filter live.

Net effect on the user complaint: cold ESP-IDF builds now show the
ninja [N/1483] progress lines streaming into the console as they
happen, instead of staring at an empty panel for 5-7 minutes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:36:58 +02:00
David Montero Crespo aae3ab1c5c feat(i18n): translate small editor remates + admin internals (Blocks 11+12)
Block 11 — small editor surfaces
- PinPickerDialog: close, filter pins, no-match, rotate / delete
  action buttons.
- RaspberryPiWorkspace: connection status (Connected / Starting…
  / Offline), Start Pi button + tooltip, Connect / Disconnect
  buttons + tooltips, Terminal tab label, file-tab close, the
  full offline overlay (title / subtitle / start CTA / two-line
  note about the staging area), terminal-loading + no-file-
  selected fallbacks.
- GitHubStarBanner: aria-label, title, body copy, Star CTA,
  dismiss button.
- ExampleLoaderPage: "Loading example…" status, "Example {{id}}
  not found." 404 line, "Browse all examples" recovery link.
  /editor and /examples links wrapped in localize().

Block 12 — admin-only internals (only admins ever see these)
- AdminBoardsTab: section headings ("By board family" / "By
  exact FQBN"), full table column headers (Family / FQBN /
  Compiles / Errors / Success rate / Runs / Distinct users /
  Distinct projects), range selector label, no-data fallback,
  load-failed error.
- AdminDashboardTab: 8 KPI cards (Total users / Total projects
  / Compiles / Runs / DAU / WAU / MAU / Public-Private), all
  chart titles + subtitles (Activity over time, Compiles by
  board family, Board diversity for pricing signal, Top FQBNs,
  Top countries with Cloudflare disclaimer, Top users / Top
  projects), table column headers, no-data fallbacks, load
  errors. Pluralised "{{count}} board(s)" via i18next plurals
  in the diversity pie chart.
- UserActivityModal: title with username interpolation, subtitle,
  range selector label, table column headers (Date / Project /
  Compiles / Errors / Runs / Saves), pluralised
  "{{count}} project(s)" line, deleted-project / no-project
  placeholder strings, no-activity empty state, load-failed.

All 8 non-English locales auto-translated via the
`scripts/translate-i18n.mjs` DeepSeek pipeline (one --force run,
~7 min). sameShape() validation passed on every output.
2026-05-09 16:22:16 -03:00
David Montero Crespo 99156c9c9d feat(editor): translate Oscilloscope + property dialog + selection bar + console + custom chips + sensor + board picker (Editor block 9)
This commit closes the cluster of small editor surfaces that touch
the active simulation experience. Every visible control on these
panels now reads from t() keys.

Translated:
- Oscilloscope panel (title, Add Channel button + tooltip, Time/div
  label, Run / Pause toggle copy + tooltips, Clear, empty-state copy
  + hint, per-channel remove tooltip).
- ComponentPropertyDialog (close, pin-roles header with two
  wire-mode variants, Arduino Pin label, rotate / delete buttons +
  the inline confirm-delete prompt with name interpolation).
- SelectionActionBar (toolbar aria-label, Rotate / Delete / Deselect
  with kind-aware delete labels for wire / component / board).
- CompilationConsole (Output title, error / warning badge counts
  with i18next pluralisation, filter dropdown, autoscroll label,
  Clear + Close icon tooltips, empty-state).
- CustomChipDialog (header with chipName interpolation, Examples /
  Editor tabs, Attributes panel header, compile status messages
  including the "✓ Compiled — N KB" success line, footer
  Cancel / Save & Place / Compile first buttons).
- SensorControlPanel (close button).
- BoardPickerModal (Add Board heading).

Translation pipeline
- en.json gets the new keys hand-curated.
- The 8 non-English locales were auto-translated via DeepSeek
  using the existing scripts/translate-i18n.mjs pipeline (one
  --force run, ~1 min total). Output validated with sameShape()
  before write so any LLM-introduced key drift would have failed
  loudly.

Quality note
- DeepSeek's translations now cover the entire bundle, including
  earlier hand-translated content. Tone may differ slightly from
  the prior hand passes but the meaning is consistent and brand /
  technical terms (Velxio, ngspice-WASM, ATmega328P, ESP32-C3,
  etc.) are preserved unchanged in every locale per the prompt
  invariants.
2026-05-09 12:25:30 -03:00
David Montero Crespo ea290d0c22 feat(editor): translate InstallLibrariesModal + SerialMonitor (Editor block 8)
InstallLibrariesModal — auto-install prompt that fires when an
example needs libraries:
- Title, subtitle (with the "Installing X of Y" progress
  interpolation, the all-done success state, and the singular /
  plural prompt explaining the requirement count).
- Per-row status badges (pending / installing… / installed /
  error) plus the Wokwi-hosted-library tooltip.
- Footer buttons: Close / Skip / "Install All ({{count}})" with
  loading variant.

SerialMonitor — multi-board tabbed serial console:
- Empty-state when no board is on the canvas.
- Right-side tab controls: Autoscroll checkbox, Clear button +
  tooltip.
- The "(Open IoT Gateway)" inline link rendered next to detected
  AP IP addresses.
- Output-area placeholders for the running-but-no-data and
  before-start states.
- Send button + input placeholder (different copy for MicroPython
  REPL vs raw Serial input).
- The line-ending dropdown options (None / Newline / Carriage
  return / Both).

Hand-translated for all 9 locales. Hotkeys (Ctrl+C) and dropdown
values stay untranslated (constants the firmware reads).

Pending in Phase 3:
- Oscilloscope panel, custom-chip dialog, sensor control panel.
- ComponentPropertyDialog (per-component property forms).
- Admin / Profile / Project pages.
- Long-form docs prose (DocsPage 2715 lines, AboutPage long
  paragraphs).
- 15 SEO landing pages (intentionally English for keyword targeting).
2026-05-09 11:35:22 -03:00
David Montero Crespo c1b0398c0d feat(editor): translate ComponentPicker + LibraryManager modals (Editor block 7)
ComponentPickerModal — the "Add Component" dialog:
- Header (title + close button), search input placeholder + clear,
  category tabs (All Components / Boards), loading + empty-state
  copy, "Clear filters" button.

LibraryManagerModal — the Arduino library browser:
- Window title, Search / Installed tabs, filter input placeholder.
- Search-tab states: searching-for-query, generic loading, no
  results (with optional query interpolation).
- Per-library row: "by {{author}}" caption, Install / Installing /
  Uninstall / Uninstalling button labels.
- Installed-tab empty-state with the prompt to use the Search tab.

Brand and product names left untouched ("LIBRARY MANAGER" stays
all-caps in English; the localised variants follow each language's
convention for product UI titles). Hand-translated for all 9
locales.

InstallLibrariesModal still pending — it's the auto-install
prompt that fires when an example needs libraries; smaller scope
but lives in the same area.
2026-05-09 11:29:07 -03:00
David Montero Crespo 4df81a3eda feat(examples): translate ExamplesPage + ExamplesGallery to 9 locales
The /examples gallery is fully localised:
- Header (heading + subtitle).
- Search input placeholder + aria-label + the clear button.
- Match-count tag with i18next pluralisation (handles _one /
  _other and Russian's _few / _many).
- Category and Difficulty filter labels + their button labels
  (basics / sensors / displays / communication / games / robotics
  / circuits; beginner / intermediate / advanced).
- Per-card "Copy shareable link" tooltip.
- Empty-state copy with two variants (with-search / without-
  search) interpolating the search query.
- Reset-filters button.
- The library-install progress overlay copy from
  ExamplesPage.tsx ("Installing libraries (N/M)") with done/total
  interpolation.

Internal /editor link uses localize() so a Spanish reader who
clicks an example lands on /es/editor.

Hand-translated for all 8 non-English locales. Per-example titles
+ descriptions are NOT i18n yet — they live in the
src/data/examples* tables and would need a separate pipeline.
DocsPage (2715 lines of prose) deferred too — best handled by
running scripts/translate-i18n.mjs once the keys are extracted.
2026-05-09 11:24:53 -03:00
David Montero Crespo ecc35f72cb feat(editor): translate SimulatorCanvas header + remove dialog (Editor block 4)
The canvas header (the bar above the simulation area) and the
"Remove board?" confirmation dialog now read from i18n.

Translated:
- Status dot tooltip (Running / Stopped).
- Active board selector tooltip + "No board" placeholder + the
  hint that prompts the user to add a board.
- Undo / Redo buttons: aria-label, dynamic title with the action
  description and the empty-state fallback. Action descriptions
  themselves stay untranslated (they come from the editor history
  store as English literals — translating them would mean reaching
  into a different store; deferred).
- Serial Monitor and Oscilloscope toggles (button title + label).
- Zoom in / out / reset-view buttons.
- Component count tooltip + Add Component button.
- The error-banner Dismiss button.
- "Remove board" item in the right-click menu, with a localised
  "(N wires)" parenthetical via i18next pluralisation.
- The full removal confirmation dialog: title with board label
  interpolation, body copy with optional connected-wires sentence,
  Cancel + Remove buttons.

Pluralisation uses i18next's _one / _other (and _few / _many for
Russian) suffixes so wire counts read naturally per language.

Hand-translated for all 8 non-English locales. Untouched (deferred):
the property dialog, custom-chip dialog, sensor control panel, and
the various inline tooltips on board pins and wire endpoints —
those are denser and benefit from a separate pass.
2026-05-09 03:20:36 -03:00
David Montero Crespo fa4f3d6e80 feat(editor): translate Save / Share / LoginPrompt modals (Editor block 3)
The three modal dialogs that fire during the editor's routine save +
share + auth-required flows are now fully localised across all 9
locales.

LoginPromptModal
- Title, body, and the three buttons (Sign in / Create account /
  Cancel). Sign in / Sign up Links use localize() so a Spanish
  reader prompted to log in lands at /es/login rather than dropping
  back to English.

SaveProjectModal
- Title (toggles between Save / Update), name + description fields
  with placeholders, save button (toggles between Save / Update /
  Saving…), Cancel button.
- Visibility toggle: Public / Private label + hint copy under the
  icon.
- All four error paths now go through t() with a {{status}}
  interpolation for the generic HTTP failure message.

ShareModal
- Title, public/private label + hint pair, "Make private" /
  "Make public" toggle, Copy button, the warning shown when the
  project is private, and the Close button.

Hand-curated translations for all 8 non-English locales. Status
codes (403) and shortcut markers preserved.
2026-05-09 03:12:52 -03:00
David Montero Crespo 7f0ac2a74c feat(editor): translate FileExplorer + FileTabs to 9 locales (Editor block 2)
FileExplorer (sidebar)
- Workspace header label and the new-workspace / save-project icon
  buttons now read from t('editor.fileExplorer.*').
- Per-board section: collapse / expand toggle, status dot tooltip
  (Running / Compiled / Idle), per-board "new file" button, and the
  composite "<board name> — click to edit" hover title (the board
  name itself stays untranslated — it's a product noun like
  "Arduino Uno").
- File rows: hover title with optional "(unsaved)" suffix,
  unsaved-dot tooltip, and the right-click context menu's Rename /
  Delete commands.
- Empty-state placeholder when no boards are on the canvas.
- The window.confirm() shown before deleting a file now reads from
  t() too, so non-English users see the prompt in their language.

FileTabs (open-tabs strip above the editor)
- Per-tab close button title and the unsaved-changes dot tooltip.
- Inline confirm dialog when closing a modified file: prompt copy,
  "Close anyway" and "Cancel" buttons.

Hand-translated for all 9 locales. Hotkey hints (Ctrl+S, Strg+S)
localised per German convention; other locales keep "Ctrl+S" as the
universally-recognised label.
2026-05-09 03:08:51 -03:00
David Montero Crespo 11012ec0e1 feat(editor): translate EditorToolbar to 9 locales (Editor block 1)
The top toolbar of the editor is now fully localised — compile / run /
stop / reset, the compile-all / run-all variants when multiple boards
are open, the language-mode select, libraries / import / export /
upload-firmware actions, and the missing-library hint banner.

Strings live under editor.toolbar.* in src/i18n/locales/<locale>/common.json.
Hand-translated for all 8 non-English locales. Brand and product
names (Arduino, MicroPython, Ctrl+B, .hex / .bin / .elf / .ihex,
GitHub Sponsors) preserved as-is.

Editor strings still pending: file explorer, file tabs, simulator
canvas, component picker, library manager modal, save / share /
project modals.
2026-05-09 03:05:51 -03:00
David Montero Crespo cc077c09d1 feat(i18n): react-i18next foundation + 9-locale support for header / footer
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.

Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
  native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
  cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
  Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
  the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
  localizedPath / switchLocale / blogUrlFor — match the blog's helpers
  one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
  inlined synchronously for first paint; non-default locales are
  lazy-loaded via dynamic import on demand. Initial locale is decided
  in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
  On every URL change loads the matching locale bundle, calls
  i18n.changeLanguage, writes the cookie, and mirrors the locale onto
  <html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
  useLocalizedHref, useLocalizedNavigate hooks for components that
  build internal links.

Routing
- App.tsx: route table extracted to a single ROUTES array, then
  registered twice — once at the root (default English) and once
  nested under each non-default locale (`/<locale>/...`). Explicit
  per-locale parent routes (rather than a generic `:lang` param) so
  React Router never accidentally swallows a real top-level path
  like `/circuit-simulator` as a locale segment.

Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
  LanguageSwitcher.astro. Globe icon + locale code on the trigger,
  native names + ISO codes in the menu. Click → `switchLocale()`
  rewrites the URL under the new locale; LocaleSync handles the
  rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
  goes through `t('header.nav.*')`, `t('header.auth.*')`. All
  internal Links wrapped with localize() so navigation stays
  inside the active locale. Added a "Blog" link computed via
  `blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
  t('footer.about').

Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
  buttons, footer About copy. Hand-translated for all 9 locales,
  AGPLv3 / brand names preserved as-is.

Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
  takes the en.json bundles and auto-translates them to the 8 other
  locales via DeepSeek (primary) + Gemini (fallback). One LLM call
  per (locale, namespace) pair. Run after extracting new strings
  with `npm run translate:i18n`.

Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
  library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
  strings have been extracted into JSON keys.
2026-05-09 00:40:37 -03:00
David Montero Crespo b42f815b49 feat(components): swap BMP280 + ATtiny85 to fritzing art
The hand-drawn SVGs in Bmp280Element.ts and Attiny85Element.ts were
functional but obviously amateur next to a real Fritzing-drawn part.
Both components now mount the equivalent Fritzing breadboard SVG as a
public static asset (`<image href>` in the shadow DOM SVG), with pin
coordinates remapped to the new artwork and pin-name labels overlaid
on top so the user can still read each connector at a glance.

frontend/public/component-svgs/bmp280.svg (new)
  Verbatim copy of third-party/fritzing-parts/svg/core/breadboard/
  bmp180_breadboard.svg. The Adafruit BMP180 breakout is the
  mechanically identical Bosch predecessor — same I2C interface,
  same 4-pin pinout. Pin labels lifted from the matching .fzp.

frontend/public/component-svgs/attiny85.svg (new)
  Verbatim copy of the Fritzing ATtiny85 DIP-8 breadboard art.

Bmp280Element.ts
  Width 80×100 px (Fritzing aspect 28.35:35.43 ≈ 0.8:1, exact uniform
  scale of 2.822 px/mm). Pin coords for SDA / SCL / GND / VCC matched
  to the connector centres in the source SVG. Pin labels overlaid on
  top. Existing wired example (esp32-bmp280) re-routes automatically
  because the wire system reads coords by pin name from pinInfo.

Attiny85Element.ts
  Width 160×132 px (Fritzing aspect 28.801:23.768 ≈ 1.21:1, exact
  uniform scale of 5.555 px/mm). The Fritzing layout puts pins on the
  TOP and BOTTOM edges (4 each), not LEFT and RIGHT like the older
  hand-drawn version. Pin coords land on clean numbers
  (x ∈ {20, 60, 100, 140}, y ∈ {6, 126}). Built-in LED on PB1 stays
  as an overlaid circle outside the chip body.
  Wires in the existing attiny85-* examples re-route automatically by
  pin name; external components positioned to the right of the chip
  may need a manual nudge for clean routing — but they work.

frontend/src/components/simulator/BoardOnCanvas.tsx
  attiny85: { w: 160, h: 100 } → { w: 160, h: 132 } to match the new
  aspect ratio. Same width as before so the chip occupies the same
  horizontal slot in existing example layouts.

scripts/component-overrides.json
  BMP280 thumbnail updated to mirror the Fritzing colour scheme
  (dark blue PCB, BMP180 silkscreen, four gold connector circles)
  so picker and canvas feel consistent.

frontend/public/components-metadata.json
  Regenerated.

docs/THIRD_PARTY.md
  New "Fritzing parts library" section. Both new assets are listed
  with their upstream paths plus the CC-BY-SA licence and link to
  the parts repo. Future Fritzing copies must be added there too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:06:44 -03:00
David Montero Crespo bba935f93d fix(serial-monitor): strip ANSI escape sequences from board output
Beta testers saw raw `[0;32m` and `[0m` text mixed into the serial
monitor for several ESP32 examples. Those are ANSI SGR escapes the
ESP-IDF logger emits to color INFO/WARN lines on a real terminal. Our
<pre> renders them literally because there was no ANSI handling in
the path.

Strip the SGR sequences (`\x1b\[[0-9;]*m`) before the IP-linkifier so
the user only sees plain text. Combined with the sdkconfig change that
drops the default log level to WARN, the ESP32 output is now as clean
as the AVR output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:32:59 -03:00
David Montero Crespo 2bcc8a62ee feat(canvas): inline two-step delete confirm in ComponentPropertyDialog
Replaces the window.confirm("Delete X?") modal with a footer that flips
into a "Delete X?" prompt + Cancel / Delete pair when the user arms the
delete. Less jarring on mobile (no native dialog), keeps the user's
flow inside the property panel.
2026-05-08 12:25:13 -03:00
David Montero Crespo 0fcd6221b5 fix(canvas): use lucide-react Undo2/Redo2 for the toolbar icons
The two hand-rolled curved-arrow SVGs I drew in bd5fd18 looked off — the
arrowheads were misaligned and the curve clipped at the bottom of the
viewBox. Swapped both for the canonical lucide-react icons (Undo2 /
Redo2), which match the visual weight + alignment of the rest of the
toolbar.

lucide-react was already in the OSS deps (added when other parts of the
app started using it). No new dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:19:50 -03:00
David Montero Crespo bd5fd18756 feat(canvas): keyboard shortcuts + toolbar undo/redo buttons
UI-facing half of the undo/redo feature. Combined with the previous two
commits, Ctrl+Z (or the toolbar button) now reverses every canvas
mutation: add/remove component, move, rotate, set property, add/remove
wire.

EditorPage.tsx:
- New window-keydown effect for Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z (and the
  Cmd equivalents). Uses the same input/textarea/contenteditable guard
  as the existing Ctrl+S handler — Monaco's per-file undo and the AI
  chat composer keep their own behaviour.

SimulatorCanvas.tsx:
- Two icon buttons (undo + redo) added to the canvas header, between
  the board selector and the Serial Monitor toggle. Tooltip surfaces
  the next command's description ("Undo: Add LED (Ctrl+Z)") so the
  user knows exactly what's about to revert. Buttons disable when the
  stack is empty in that direction.
- New `canvas-icon-btn` CSS class for square 32×32 icon-only buttons
  (matches the visual weight of the existing Serial button without
  the label).
- Subscribes to history / historyIndex via store selectors so the
  buttons re-render reactively as commands are pushed/undone.

No new tests — the store-level coverage from 99ed22b already exercises
undo/redo round trips. UI affordances are wired pass-through to those
store APIs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:11:53 -03:00
David Montero Crespo df8e666aa3 feat(canvas): SimulatorCanvas routes mutations through record* actions
Wires every user-initiated canvas mutation in SimulatorCanvas to the
recorded variants added in 99ed22b, so Ctrl+Z (next commit) can roll
each one back as a single step.

Routed through record*:
- handleSelectComponent (picker → add) → recordAddComponent
- Delete keyboard handler (selectedComponentId branch) → recordRemoveComponent
- handleRotateComponent → recordRotate (still mutates live via
  updateComponent so the rotation visually applies; record stores the
  prev/next angles for undo)
- Drag → recordMove on mouseup. Captures component.x/y at mousedown in
  a new dragStartPosRef and only records on actual drag-end (skips the
  click branch that just opens the property dialog).
- Pin-click "finish wire" path → calls finishWireCreation (which
  atomically appends the wire) then pushes a CanvasCommand for that
  wire with applyNow:false (state is already at post-add).
- Selected-wire delete (keyboard + SelectionActionBar + PinPickerDialog)
  → recordRemoveWire
- Selection action bar component delete → recordRemoveComponent
- Pin picker dialog component delete → recordRemoveComponent
- ComponentPropertyDialog onPropertyChange → updateComponent applies
  live, then recordSetProperty captures prev/next so Ctrl+Z reverts the
  value without re-running the raw mutation.

Cleaned up unused destructures of addComponent / removeComponent /
removeWire from the original useSimulatorStore() call — every call site
now uses the record* equivalents.

No new keyboard shortcuts or toolbar buttons in this commit; that's
landing next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:09:20 -03:00
David Montero Crespo c20a7498fc feat: add touch-friendly components for improved mobile usability
- Implemented PinPickerDialog for selecting pins on touch devices.
- Added SelectionActionBar for managing selected items with touch actions.
- Created WireModeBanner to provide feedback during wire creation.
- Introduced useTouchDevice utility for detecting coarse pointer input.
- Refactored WireLayer to utilize useIsCoarsePointer for touch detection.
2026-05-08 11:12:26 -03:00
David Montero Crespo 70cc8c2e11 feat(editor): view-mode toggle, agent-chat slot, toolbar polish
Changes that ship to OSS — all benign for self-hosters, but most are
extension points the velxio-prod overlay (and any private fork) needs to
plug an in-editor AI chat into the page.

Editor:
- 3-way view-mode toggle (code / both / circuit) in the unified toolbar.
  Lets users hide a pane to give a right-docked sidebar (e.g. the AI
  chat overlay) more breathing room. Persisted in useEditorStore.
- Default file explorer narrower (210 → 165 px); min 110.
- Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip);
  the BoardSelector dropdown elsewhere already shows the active board.
- Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow
  menu gave up too much discoverability. Removed dead overflow state.

Simulator:
- Fix: global Delete/Backspace handler in SimulatorCanvas no longer
  fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable
  — affected any in-page text field, not just the chat overlay.

Overlay extensibility:
- New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so
  pro overlays can portal a chat panel into the editor without
  forking the page.
- vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set.
  Lets local-dev junctions (overlay tree → frontend/src/pro) resolve
  bare imports back to the OSS node_modules without resolving symlinks.

Deps:
- Added react-markdown + remark-gfm (rendered chat output) and
  @google/genai + zod (overlay agent loop). Tree-shaken from the OSS
  bundle when no pro code imports them.

gitignore:
- Ignore backend/app/pro/ and frontend/src/pro/ junctions used by
  developers running a private overlay against the OSS dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:56:51 -03:00
davidmonterocrespo24 37fbc502d1 chore(examples): drop PNG thumbnails, ship WebP only (-80% asset size)
The PNG fallbacks were carrying 80% of the gallery's bundled weight
(16 MB of 20 MB) and serving virtually no traffic — WebP is supported
on ~97% of in-use browsers, and the few hold-outs (very old Safari)
fall through to the CircuitPreview SVG mock via the existing onError
handler. No visual regression for modern browsers.

Numbers:
- before: 258 files, ~20 MB total
- after:  129 files, ~3.6 MB total (avg 28.6 KB / WebP)

ExampleThumbnail simplified: drops the <picture>/<source> wrap around
the WebP <source> + PNG fallback, just renders the WebP <img> directly
with onError → CircuitPreview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:54:33 +02:00
davidmonterocrespo24 d53f4f1380 feat(examples): real canvas screenshots as gallery thumbnails
Replaces the manually-positioned wokwi-element mock (CircuitPreview) in
the /examples gallery with real screenshots of the actual simulator
canvas, so each card shows the example's boards + components + wires
exactly as they appear when you open the example.

How it works
- A new <ExampleThumbnail> component tries
  /examples-thumbs/<id>.{webp,png} first. If the image 404s — or no
  thumbnail has been captured yet — it falls back to the existing
  CircuitPreview component. No-op for examples without a screenshot.
- ExamplesGallery and ExampleDetailPage now render <ExampleThumbnail>
  instead of CircuitPreview directly.
- Explicit example.thumbnail field still wins (kept the existing
  override path in case someone wants a custom asset).

Capture pipeline
- Generated by velxio-prod's scripts/capture-example-thumbs.mjs
  (Playwright + sharp). For each example: opens /examples/<slug>,
  clicks "Open in Simulator", waits for [data-board-id] elements,
  computes the bbox of all boards + components, sets a transform on
  .canvas-world to center and fit them with 12% padding inside the
  canvas viewport, screenshots .canvas-content, and re-encodes to
  600x360 @2x DPI as .png + .webp.

This commit ships the first batch (102 of ~129 examples — the rest
will follow once the capture completes; missing slugs gracefully
fall back to CircuitPreview in the meantime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:44:10 +02:00
davidmonterocrespo24 e0dbde0a76 feat(design): tokens, webfonts, Lucide icons, board PNG/WebP picture strip
Foundation
- Add 7 token CSS files in src/tokens/ — semantic colors, 4-pt spacing,
  Apple-HIG type ramp, radius/elevation/motion/z-index scales.
- Refactor src/index.css to import tokens and remap legacy aliases
  (--accent, --bg, etc.) onto the new --color-* semantics so existing
  components keep rendering during migration.
- Drop the duplicate font-family from src/App.css; body inherits from :root.
- Global *:focus-visible ring backed by --color-focus-ring (WCAG 2.4.7).

Webfonts (self-hosted)
- Add Inter.var.woff2 (variable, OFL) and JetBrainsMono.var.woff2 to
  public/fonts/. Preloaded in index.html with crossorigin.
- Old stack -apple-system kept as fallback so Mac users still get SF Pro.
- Fixes cross-OS rendering inconsistency (Win/Linux/Android were falling
  back to Segoe UI / Roboto, breaking the type grid).

Component primitives
- New src/components/ui/{Button,Card,Input}.tsx + .css. Built on the
  semantic tokens, ready for incremental migration of .ap-* CSS classes.

Lucide icons
- Replace 6 inline SVG icon components in LandingPage (IcoChip / IcoCpu /
  IcoCode / IcoZap / IcoLayers / IcoMonitor) with lucide-react imports.
  Aliased so call sites are unchanged. ~80 lines of inline SVG removed.
- IcoGitHub kept bespoke (filled glyph, brand-correct).

Marketing assets
- Convert top 8 boards to transparent PNG + WebP at 1x / 2x:
  Arduino Uno, Nano, Mega 2560, Pi Pico, Pi Pico W, ESP32-C3,
  ESP32-DevKit-V1, XIAO ESP32-S3.
- Migrate matching cards in LandingPage and Velxio2Page to <picture>
  with WebP > PNG > SVG fallback. Other 8 boards keep <img src=*.svg>
  for now (Raspberry Pi 3B, ESP32-CAM, etc.).
- Refresh og-image.png — same canonical URL, new content (4 hero boards
  + branding instead of generic logo card).
- Fix latent bug in LandingPage: ESP32 DevKit V1 card was loading
  esp32-devkit-c-v4.svg; now uses esp32-devkit-v1.{webp,png,svg}.

Build verified: npm run build:docker succeeds, 246 SEO pages prerender.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:45:44 +02:00
David Montero Crespo 8b14815094 feat(components): pro_only flag + registry merge API + picker gate hook
Three small additions so private overlays can add components gated behind
a paid subscription without forking the picker:

- types/component-metadata.ts: optional pro_only?: boolean field on
  ComponentMetadata. Self-hosters never set it; picker behaves identically.
- services/componentRegistry.ts: new mergeComponents() public method.
  Pro overlay calls this after the default registry has loaded to splice
  in extra components (replacing any with the same id).
- components/ComponentPickerModal.tsx: when a pro_only component is
  clicked, the picker first calls window.__velxio_pro_gate__(component)
  if defined. If the gate returns true, the click is consumed (overlay
  shows an upgrade modal). If absent or returns false, the click passes
  through to onSelectComponent as normal.

Net upstream change: ~25 lines, all backwards-compatible. OSS image
behaves exactly as before since no overlay sets pro_only or installs
the gate.
2026-05-05 10:33:53 -03:00
David Montero Crespo a5b5e57aae feat: add data-velxio-slot markers for overlay portal targets
Three small markers (each one HTML attribute) so private overlays can
portal-inject UI into well-defined places without forking the upstream
component:

- AppHeader user dropdown: data-velxio-slot="user-menu"
  Lets overlays add menu items between "My projects" and "Sign out"
  (e.g. a Privacy / opt-out item).
- AdminPage tab bar: data-velxio-slot="admin-tabs"
  Lets overlays add extra tabs alongside Dashboard / Users / Projects /
  Boards (e.g. a Pro Analytics tab).
- AdminPage tab content area: data-velxio-slot="admin-tab-content"
  Sibling div where overlay tab content can portal-render.

Generic markers, no overlay-specific code in upstream. Anyone with
private extensions can use them. The OSS build is otherwise unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:28:02 -03:00
David Montero Crespo edd2ac32d5 feat: add optional extension hooks for private overlays
Three small, backwards-compatible hooks let anyone with private features
(velxio.dev's analytics, custom integrations, paid tiers, …) layer them
on top of the open-source build without forking files.

Backend (app/main.py):
- After standard router registration, try-import an optional `app.pro`
  module exposing `register_pro(app)`. ImportError is silently swallowed
  (the OSS image doesn't ship `app.pro`, so this is a no-op there).

Frontend:
- EditorToolbar: new optional `rightSlot` prop renders extra elements
  after the built-in right-group buttons (mirrors the existing
  `centerSlot` pattern).
- main.tsx: dynamic `import('@pro/index')` gated by VITE_PRO_BUILD env.
  When unset (OSS build), the branch is dead-code-eliminated and no pro
  chunk is emitted.
- vite.config.ts: `@pro` alias resolves to `src/__pro_stub__/` by default.
  Private builds set `VITE_PRO_BUILD=true` and `PRO_OVERLAY_PATH=<path>`
  to point at their real overlay tree.
- src/__pro_stub__/index.ts: 1-line no-op `mountPro` so TypeScript and
  Vite resolvers stay happy in OSS builds.

Verified: `npm run build:docker` succeeds; `npm test` passes 1161/1162;
the OSS bundle (43 MB) contains zero references to `__pro_stub__`,
`@pro`, or `pro/index` (verified via `grep dist/`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:03:20 -03:00
David Montero Crespo 531c337d19 fix(install): unblock self-hosting + drop forced wokwi clones
Resolves several install pain points reported by users (#108, #120) and
removes the obligatory upstream-clone step that confused contributors and
slowed down every Docker build.

Install fixes:
- nginx: server_name → catch-all default_server, drop Debian's stock site
  so reverse-proxied users no longer get the "Welcome to nginx" page.
- entrypoint: auto-generate SECRET_KEY at first boot, persisted under
  data/.secret_key. backend/.env is now optional in docker-compose.yml.
- backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on
  some Python builds — caused uvicorn startup failures on WSL).

Wokwi libs come from npm:
- @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in
  frontend/package.json. Vite aliases removed.
- Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements
  / wokwi-boards. Frontend stage is just COPY + npm install + build:docker.
- Board SVGs vendored under frontend/public/boards/ (10 deduped against
  existing files, 2 truly new). third-party/wokwi-* clones become reference-
  only credits — generate-component-metadata.ts skips gracefully when absent.

Production config split out:
- docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf,
  update-third-party.bat removed. Production deployment lives in its own
  repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups
  + pinned upstream commit).

Verified locally: 1161 frontend tests pass, build:docker completes clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:04:11 -03:00
David Montero Crespo 9cd5061732 refactor: rename wokwi-libs/ → third-party/
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.

Mechanical changes:

  Path rename:
    wokwi-libs/ → third-party/
    update-wokwi-libs.bat → update-third-party.bat
    docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md

  Submodule reconfiguration:
    .gitmodules — 4 path= and section names updated
    .git/modules/wokwi-libs/ → .git/modules/third-party/
    each submodule's .git file rewired to ../../.git/modules/third-party/<name>

  Reference updates (~80 files): vite.config.ts aliases, Dockerfile
    COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
    docs/* and test/*/autosearch/* entries that mention the path,
    package-lock.json file: dependencies, .gitignore patterns,
    sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
    .dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
    (path) and bare `wokwi-libs` (textual mentions in docs/comments).

Verified:
  - npx tsc -b --noEmit produces no new errors related to these paths
  - vite.config.ts aliases now point at ../third-party/avr8js etc.
  - All 4 git submodules (avr8js, rp2040js, wokwi-elements,
    wokwi-features) are linked under third-party/ with their
    worktrees re-populated and config files referencing the new path
  - `grep -r wokwi-libs` returns zero hits outside node_modules,
    .vite, frontend/dist, third-party/ (upstream submodule contents),
    *.pyc caches, and *.dll.pre-camera rollback binaries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:58:57 -03:00
David Montero Crespo 36914e209c feat(webcam): universal compatibility — any webcam on any PC
User goal: ESP32-CAM live preview that works with any webcam,
regardless of resolution, brand, or scene complexity. The previous
fixed-quality 0.28 was fragile (intermittent decode errors on
moving/textured scenes) and capped visual quality unnecessarily.

Two-layer fix; either alone is insufficient:

LAYER A — Bounded JPEG encoder (frontend, this repo)
  frontend/src/hooks/useWebcamFrames.ts:
    encodeBoundedJpeg() walks a quality ladder [0.6, 0.5, ..., 0.1]
    until the JPEG fits in MAX_FRAME_BYTES (23 000). If even q=0.1
    overshoots — extreme HD/4K scenes — falls back to a 240×180
    downscaled canvas at q=0.4. Guarantees every emitted frame fits
    the deliverable budget regardless of webcam hardware.

    The hook now exposes lastQualityUsed + lastDownscaled so UI can
    surface when auto-tuning kicks in.

  frontend/src/components/simulator/CameraToggle.tsx:
    Tooltip shows "(auto-tuned to q=0.X)" or "(auto-downscaled, q=0.X)"
    while streaming so users see what the encoder picked.

LAYER B — Multi-lap descriptor ring walker (qemu-lcgamboa, submodule)
  Bumps the QEMU per-frame deliverable cap from 8 KiB to ~32 KiB by
  letting the walker reset the descriptor ring up to 4 times per
  VSYNC. Submodule pointer bumped to eb8b7a5d.

Combined, the demo now supports:
  - Cheap 480p webcams: q=0.6, 5-10 KiB JPEGs, sharp
  - Logitech mid-range:  q=0.5-0.6, 8-15 KiB JPEGs, sharp
  - HD 1080p webcams:    q=0.4-0.6, 15-23 KiB JPEGs, sharp
  - 4K complex scenes:   downscaled, still readable

Documented as bug closure in:
  test/test-esp32-cam/autosearch/15_universal_webcam_compat.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:48:24 -03:00
David Montero Crespo c068612077
Merge pull request #137 from davidmonterocrespo24/esp32-cam
Esp32 cam
2026-05-02 22:40:06 -03:00
David Montero Crespo 155b962c21 feat(gallery): add 2 ESP32-CAM examples to the in-app gallery
Adds two listed examples to the gallery (book icon → Examples) so
users can one-click load the new ESP32-CAM emulation:

1. ESP32-CAM: Webcam Demo (sensors / beginner)
   Minimal sketch — init OV2640, verify SCCB chip-id, loop on
   esp_camera_fb_get() printing frame metadata to Serial. Proves
   the emulation is alive without any external components.

2. ESP32-CAM + ILI9341 Live Preview (displays / intermediate)
   Full demo — decode JPEG with jpg2rgb565() (built-in to
   esp32-camera/conversions, header exposed by the Velxio compile
   template) and render the resulting RGB565 bitmap to a 320×240
   SPI TFT. Pre-wired diagram: ILI9341 connected via VSPI to GPIOs
   12-15 (the only block free after OV2640 takes over the rest of
   the AI-Thinker pins).

Type changes:
- ExampleProject.boardType union extended with 'esp32-cam'
- BOARD_TABS in ExamplesGallery.tsx gets a new "ESP32-CAM" tab
  (orange #d35400)

Both examples use boardFilter: 'esp32-cam' so they show under
the new tab and not the generic ESP32 one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:21:19 -03:00
David Montero Crespo e73c1d341c feat: ESP32-CAM emulation with webcam frame bridge
First open-source end-to-end emulation of the AI-Thinker ESP32-CAM
in QEMU, paired with a browser webcam → firmware bridge so users can
develop camera sketches without hardware. Status: esp_camera_init()
returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly
match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by
the upstream driver. Final piece (cam_task accepting frames) is in
progress — descriptor walker fix landed in this commit.

Backend (Python/FastAPI):
- simulation.py: camera_attach/frame/detach WS handlers
- esp32_worker.py: ctypes binding to velxio_push_camera_frame +
  feature-detection fallback for older DLLs
- esp32_lib_manager.py: forward camera commands to the worker stdin
- esp-idf-template/main/CMakeLists.txt: esp32-camera headers added
  via add_prebuilt_library + REQUIRES driver (resolves i2c_master_*
  symbols). LED_BUILTIN=2 fallback for sketches that hardcode it.

Frontend (React/TS):
- EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family)
  added to isQemuBoard list — Run button now starts the QEMU bridge
  for these boards instead of falling through to the AVR path
- useWebcamFrames.ts: getUserMedia → OffscreenCanvas →
  toBlob('image/jpeg') → base64 → WS at ~10 fps
- CameraToggle.tsx: header button with status colors + frame counter
- SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards
- Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa
- useSimulatorStore.ts: diagnostic log on compileBoardProgram
- components-metadata.json: regen including esp32-cam component

Submodule pointer:
- wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on
  davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32)

Investigation + tests in test/test-esp32-cam/:
- 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S
  spec, build blueprint, blockers resolved, descriptor walker fix)
- 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip,
  webcam_demo) + 8 live + WS regression tests
- README with the user-facing flow

.gitignore:
- libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated)
- wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path,
  not part of this repo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:29:01 -03:00