Commit Graph

571 Commits

Author SHA1 Message Date
David Montero 3a2abc48d9 fix(espidf): accurate core-lib warnings + gateway open hook
Two unrelated polish fixes.

espidf_compiler: headers that resolve to an arduino-esp32 CORE lib
(WebServer, WiFi, …) were correctly skipped from the user-lib merge but
then fell through to a scary "Library for <X> not found — build may
fail" warning — even though the build succeeds because the symbols are
compiled into the core. Now logs an accurate "provided by arduino-esp32
core — already compiled in, not merging". Same treatment for core
headers that aren't standalone lib dirs (Udp.h, IPAddress.h,
WiFiUdp.h, …) via a new _CORE_ESP32_HEADERS allowlist.

SimulatorCanvas: the WiFi badge's "open IoT gateway" click now consults
an optional window.__velxio_iot_gateway_open_gate__ hook before opening
the gateway tab. A private overlay can install it to gate the gateway
behind a paid plan and show an in-place upgrade modal instead of dumping
a 402 page in a new tab. OSS builds have no hook → opens normally. The
check is synchronous so it doesn't trip popup blockers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 20:38:03 +02:00
David Montero 44f12f0e53 feat(seo+nav+community): public-project indexing + classroom nav + examples grid
Search-engine indexing of public projects
- Update robots.txt to also list /sitemap-projects.xml so Googlebot /
  Bingbot discover every public project's canonical /:username/:slug URL.
- Add /docs/github-sync + /classroom entries to seoRoutes.ts so the
  build-time sitemap.xml picks them up.

Navigation polish
- AppHeader gains a "For schools" link between Pricing and Download.
- LandingPage's pricing section gets a slim banner under the cards
  pointing institutional visitors to /classroom (visible discovery path,
  not just a footer link).
- Localised header.nav.classroom + landing.pricing.classroomBanner +
  landing.pricing.classroomCta across all 9 maintained locales (en/es/
  pt-br/fr/de/it/ja/ru/zh-cn).

Community examples
- New CommunityProjectsGrid component lives next to ExamplesGallery on
  /examples.  Fetches /api/projects/featured (Pro-overlay-only endpoint)
  and renders the top public projects ranked by run_count.  Quietly
  hides itself when the endpoint returns nothing or fails, so the OSS
  build still ships cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 18:11:28 +02:00
David Montero 0bcfbde617 feat(seo+landing): /classroom in footer + seoRoutes for sitemap inclusion
LandingPage footer gains a "For schools" link sitting between Pricing
and About — gives institutional visitors a discoverable path to the
Classroom landing without burying it inside the FAQ.

seoRoutes.ts adds the /classroom entry (priority 0.85, monthly
changefreq) so the auto-generated sitemap picks it up on every build.
Bonus: getSeoMeta('/classroom') now returns the institutional title +
description if any other code wants to read it programmatically.

The static public/sitemap.xml is not committed — `npm run generate:sitemap`
overwrites it during the Docker build, so any hand-edit would be wiped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:21:41 +02:00
David Montero 1d847d19cc fix(landing): pricing card prices, names + features for the shipped tiers
The home page's pricing section was still showing the dropped Phase 0
shape (Free / Pro $15 / Pro Max $35) instead of what /pricing and the
billing backend actually serve (Free / Maker $7 / Pro $19).

  - Replace the middle card from "Pro $15" → "Maker $7" (CTA "Start
    Maker") + the right card from "Pro Max $35" → "Pro $19" (CTA
    "Subscribe to Pro").  "Most popular" badge moves to the now-Pro
    card (still the upsell sweet spot).
  - i18n keys renamed in lockstep: tiers.pro → tiers.maker, tiers.pro_max
    → tiers.pro.  Updated in all 9 locales (en/es/pt-br/fr/de/it/ja/ru/
    zh-cn) with translated copy that mentions the actually-shipped Pro
    perks (private projects, GitHub Sync, BOM CSV, schematic PNG,
    watermark-free embed).  The Spanish line about "Maker" is left as
    the loanword so it stays consistent with /pricing.

No backend changes — quota.py PLANS was already correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:00:02 +02:00
David Montero d34fc2059d feat(seo): llms.txt for AI search engine indexing (D5.6)
Adds the proposed llmstxt.org file under frontend/public/ so the SPA
nginx serves it at https://velxio.dev/llms.txt.  ChatGPT-driven traffic
has had the highest engagement of any channel (82% session quality per
GA), so feeding the AI crawlers a curated, machine-readable summary
is high-leverage: the model gets accurate tier prices, supported
boards, comparison framing vs Wokwi / Tinkercad / Proteus, plus FAQ
answers — instead of stitching together a fuzzy view from blog posts.

Notable departures from the original phase-5 draft:
  - Tier shape is the shipped one (Free / Maker $7 / Pro $19), not the
    proposed Pro/Hobbyist + LemonSqueezy variants that never landed.
  - Geo-pricing section dropped (Phase 2 deferred — same reason).
  - Supported boards list reflects the actual MCU emulator coverage in
    the latest velxio image, not the aspirational roadmap.
  - GitHub Sync and embed iframe (D3.5) are now first-class features.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:54:29 +02:00
David Montero 5caf40fdeb feat(editor-toolbar): Share / Embed menu item
Fifth item in the overflow menu next to Sync to GitHub.  Free for
all users (no PRO badge); dispatches velxio-pro-share-prompt with the
current project id so the overlay's ShareModal can render the direct
link + iframe snippet copy UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:36 +02:00
David Montero f4abad4ca2 feat(editor-toolbar): D5 — "Sync to GitHub" item in overflow menu
Dispatches velxio-pro-upgrade-prompt's sibling event
velxio-pro-github-sync-prompt with the current project id.  The pro
overlay's GithubSyncModal listens and runs the four-state link/sync
flow (no-pro / not-connected / not-linked / linked) inline without
leaving the editor.

Pure OSS builds have no listener so the click is a silent no-op —
those users can't have linked repos anyway.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 08:03:39 +02:00
David Montero 8ecbe89609 feat(editor-toolbar): in-place Pro upgrade prompt + progressive overflow menu
Replace the hard /pricing redirect on 402 with a window-dispatched
'velxio-pro-upgrade-prompt' event so private overlays can surface an
in-editor upgrade modal instead of bouncing the user out of context.

Move BOM, Schematic image and firmware upload buttons into a "..." More
menu next to the existing Export ZIP icon, freeing two button slots in
the inline toolbar.  Mark the two premium items with a small "PRO" pill
so free-plan users know they're gated before they click — Notion- /
Linear-style discoverability cue.

Also wire Import + Export ZIP to fall back into that same menu once the
toolbar container drops below 320 / 280 px (container queries on the
editor pane width).  Mobile / narrow-split layouts keep full feature
parity through the dropdown instead of overflowing into a horizontally
scrolling row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 07:41:05 +02:00
David Montero 9a40de78c0 feat(share-modal): D1.4 — 3-level visibility picker (public/unlisted/private)
Phase 1 D1.4 — replaces the binary public/private toggle in ShareModal
with three radio-button-styled options. Optimistic UI: every option
renders for every user; the backend's 403 (with structured
visibility_not_allowed detail) redirects to /pricing?from=visibility_X
so the pricing page can lead the right pitch.

Why optimistic-then-redirect instead of hiding/locking options:

  1. Discovery — Free / Maker users SEE Pro unlocks Private. That's the
     exact conversion signal the pricing page is trying to surface.
  2. Discovery without surprise — the locked click goes to /pricing
     with a hint, not a dead modal.
  3. Less plan-coupling — this upstream component doesn't need to know
     about the pro overlay's plan store. Backend is the only source of
     truth for what's allowed.

Touched:
  - ShareModal.tsx: full rewrite as a 3-option picker with badges
    (Maker / Pro) on the gated options.
  - projectService.ts: ProjectResponse / ProjectSaveData now declare
    `visibility?: 'public' | 'unlisted' | 'private'`. is_public stays
    declared for backward compat with old callers.
  - useProjectStore.ts: CurrentProject gains `visibility?`; setVisibility
    accepts EITHER the legacy boolean OR the new enum and keeps both
    fields coherent.
  - common.json (4 locales): new editor.share.visibility.{publicLabel,
    publicHint, unlistedLabel, unlistedHint, privateLabel, privateHint}
    + editor.share.updateFailed.

Backend gating + DB migration are in the velxio-prod pro overlay
(commit referencing this submodule pointer).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 05:49:03 +02:00
David Montero c957767e2e feat(editor): add Export-Screenshot button (Phase 3 D3.2)
Front-end half of the schematic image export. New camera-icon button in
the editor toolbar between BOM and Upload-Firmware. Handler:

  1. POSTs to /api/pro/projects/{id}/screenshot.png (server renders the
     canvas with headless chromium, returns a PNG).
  2. 402 → /pricing?from=screenshot_export
  3. 401 → /login with redirect-back
  4. 422 → friendly "add at least one component" toast
  5. 200 → blob download with Content-Disposition filename
  6. The "rendering..." toast surfaces during the 5-10 s of headless
     chromium time so users know to wait, not click again.

i18n key editor.toolbar.exportScreenshot added in en/es/pt-br/zh-cn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 04:18:43 +02:00
David Montero b5a80a65b9 feat(editor): add BOM-export button + handleExportBom flow
Phase 3 D3.1 — front-end half of the BOM export. The toolbar gains a
new spreadsheet-icon button next to the existing project-export button.
On click:

  1. POST is NOT used — the backend endpoint is GET-based and streams a
     CSV. We just open the URL.
  2. 402 (Pro-required) routes the user to /pricing?from=bom_export
     so the page can show the right upgrade narrative.
  3. 401 routes to /login with redirect-back.
  4. 200 triggers a Blob download with Content-Disposition filename.

i18n key editor.toolbar.exportBom added in en/es/pt-br/zh-cn — the
" — Pro" suffix on the tooltip hints at the gating without forcing the
user to discover it only on click.

The button is shown to everyone, not hidden by plan. Free/Maker users
clicking it gets the 402 route to /pricing, which is intentional — that
is the upgrade-discovery funnel we want, not a silent locked icon.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 03:12:52 +02:00
David Montero 6df53a0d32 i18n: refresh landing.hero subtitle + trustLine with AI/offline/AGPLv3 USPs
Phase 1 D1.8 — the home-page hero leads with three defensible
differentiators Velxio has that the dominant alternative doesn't:

  - AI agent integrated (Wokwi has none)
  - Works offline as a desktop app (Wokwi is cloud-only)
  - AGPLv3 open source (Wokwi is proprietary)

Subtitle and trustLine rewritten across all four shipped locales
(en, es, pt-br, zh-cn). No layout change — the LandingPage.tsx
component renders both strings already.

The competitor name isn't mentioned anywhere — the user comparing
side-by-side does the math themselves. Anchoring on the USPs makes
the eventual /pricing visit ("Maker $7 = AI included") land in
context instead of cold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:57:23 +02:00
David Montero 0beee4af3d i18n: add quotaModal block in en/es/pt-br/zh-cn (10 keys × 4 langs)
The quota-exhausted modal (rendered by the velxio.dev pro overlay when
a free user hits the daily AI cap) was hardcoded English. The modal is
seen by users from CN/BR/MX/CO/AR/PE/IN — the audiences most likely to
bounce on English-only UX. This adds the four key languages.

Keys:
  titleFree    — "You've hit today's free limit"
  titlePaid    — "You've reached your daily limit"
  bodyFree     — explainer + Pro upgrade pitch (interpolates cap/proCap/multiplier)
  bodyPaid     — explainer for paid users who hit their own tier's cap
  today / thisMonth / resets — stats labels
  ctaUpgrade   — primary CTA ("Upgrade to Pro — $15/mo")
  ctaSeePlans  — fallback CTA for non-free users
  ctaWait      — secondary "Wait until reset"

Upstream-only change — the velxio-prod overlay's AgentChatPanel.tsx is
wired to consume these via useTranslation in a separate commit.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:57:29 +02:00
David Montero 0d37472aa3 test(esp32): add hardResetPinStates to PinManager mocks in 6 test files
Same root cause as the previous test fix in 3e38397 — upstream commit
d64eebc (fix(stop): reset CPU to PC=0) added a hardResetPinStates() call
to useSimulatorStore.stopBoard. The vi.mock factories in these 6 ESP32-
adjacent test files only exposed updatePort/onPinChange/getListenersCount,
so any test path that hits stopBoard crashed with "is not a function"
once the real prod code called the new method.

Each gets a single-line addition: this.hardResetPinStates = vi.fn();

Verified with full vitest run: 127 files pass, 2,005 tests pass, 0 failures.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:31:43 +02:00
David Montero 3e38397366 test(frontend): fix 2 test-mock bugs that blocked the deploy gate
1. multi-board-integration.test.ts — PinManager mock was missing
   hardResetPinStates(). Upstream commit d64eebc (fix(stop): reset CPU
   to PC=0) added that method to PinManager and useSimulatorStore.stopBoard
   calls it, but this test's vi.mock factory never exposed it. Result:
   "TypeError: getBoardPinManager(...)?.hardResetPinStates is not a function"
   even though the optional chain looks safe — the chain only short-circuits
   on null/undefined, not on a non-function property.

2. vitest.config.ts — was missing the @velxio alias that vite.config.ts
   defines. defineConfig from vitest/config does NOT auto-inherit from
   vite.config.ts; the alias has to be re-declared. Without it, overlay
   tests importing @velxio/store/useEditorStore failed with "Cannot find
   package '@velxio/...'" even though the build (which DOES inherit the
   alias) resolves them fine.

Verified: full set of 3 previously-failing tests now pass cleanly
(multi-board-integration: 43 passed, snapshot: 0, pinIntrospection: 10).

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:26:21 +02:00
David Montero bab33e9e55 Merge commit 'e6df4ae8acacce956c1b112123ec7e00438c4bd5' 2026-05-27 08:34:17 +02:00
David Montero 8437f70770 feat(header): add Download nav link → /account/desktop-install
Inserts a Download entry between Pricing and Blog in the main nav.
Routes to the existing DesktopInstallPage in the velxio-prod pro
overlay (auth gate + platform-detect + signed-licence download flow).

i18n: header.nav.download added across all 9 shipped locales
(en/es/ja/it/de/ru/pt-br/zh-cn/fr) with native translations.

Self-hosted OSS image: the route doesn't exist there, so the link
lands on the upstream router's 404 — same fallback behaviour as
/pricing already has for self-hosters. Acceptable until the OSS
side gets its own placeholder.
2026-05-27 08:34:14 +02:00
David Montero Crespo e6df4ae8ac feat(flash): write compiled sketches to real USB boards (phases D1+D3)
Brings hardware flashing into Velxio Desktop. Per-board "Flash to
real board" entry in the canvas context menu opens a modal that
enumerates USB serial ports, lets the user pick one, then
streams arduino-cli upload output live until the board is flashed.

Backend (Phase D1) — backend/app/api/routes/flash.py (new):
  POST /api/flash/upload  (multipart: board_id, port, fqbn,
                           program_format, program)
  → SSE stream of {phase, line?, progress?} events
  → final {phase:'done', success, elapsed_ms, error?}

  - Wraps `arduino-cli upload -p <port> -i <file> --fqbn <fqbn> -v`
    so AVR (avrdude), ESP32 (esptool), RP2040 (picotool), SAMD
    (bossac) all share one code path — arduino-cli internally
    dispatches by FQBN.
  - Per-port asyncio.Lock prevents two simultaneous flashes from
    fighting over the same /dev/ttyACM0.
  - Allow-list of FQBN prefixes (arduino:avr, ATTinyCore:avr,
    rp2040:rp2040, esp32:esp32, arduino:samd) so a typo can't
    cause a confusing arduino-cli error.
  - Format allow-list (hex / bin / uf2 / elf) drives the temp
    file extension - arduino-cli uses the extension to route to
    the right uploader.
  - 8MB hard cap on the uploaded program (real sketches are
    well under that; protects against a runaway frontend).
  - X-Accel-Buffering: no header so nginx doesn't hold the SSE
    chunks until the flash completes.

Frontend (Phase D3):
  - frontend/src/services/flashService.ts (new):
      async generator streamFlash() yields parsed SSE events.
      Handles the base64-vs-text gotcha (compile returns hex_content
      as text but binary_content as base64; for binary formats we
      atob() into a Uint8Array before posting so the form upload
      sends actual bytes, not the base64 ASCII).
  - frontend/src/components/simulator/FlashModal.tsx (new):
      Three-state UI: picking (port dropdown), flashing (progress
      bar + live log), success/error (verdict + retry).
      Empty-ports state shows a Linux dialout-group hint.
  - SimulatorCanvas.tsx: board context menu gains "Flash to real
    board" entry, gated on isTauri() + presence of compiledProgram.
    Hidden in web (WebSerial is a separate sprint).
  - tauriBridge.ts: SerialPortInfo type + listSerialPorts() helper
    that invokes the Rust shell command added in Phase D2.

The sidecar already has arduino-cli on PATH (per
`pro/desktop/sidecar/main.py::_expose_bundled_arduino_cli`), so
no installer changes are needed — flash works the moment the
0.4.x desktop bundle ships with these commits.

Plan + remaining phase tracked in project/hardware-flashing/.
D2 (Rust serial enum) committed separately as a Tauri-shell-only
concern; D4 (manual smoke matrix with real boards) requires
physical hardware so it stays a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 00:20:20 -03:00
David Montero Crespo 145710561a fix: 5 user-reported issues (#208 #209 #210 #211 #212)
#208 — stale binary executes after compile error
EditorToolbar.handleCompile: on failed compile, clear the active
board's compiledProgram so a subsequent Run can't silently execute
the previous successful build (which doesn't match the editor any
more). The Run gate already short-circuits on !compiledProgram and
forces a fresh compile.

#209 — compile terminal kept stale messages across runs
EditorToolbar.handleCompile: setCompileLogs([]) at the top of the
handler. Previously logs from the prior compile lingered, making it
hard to tell new errors / warnings apart from old ones.

#210 — desktop File > New Project did nothing
desktop/menu.ts: the menu action used to dispatch a CustomEvent
nobody listened to. Replaced with a real `newProject()` function
that stops the running simulation, removes every board (also drops
the bridges + wires touching them), clears components / wires,
loads the default Blink sketch into the editor, clears project
metadata, and wipes the compile output. Confirms first if there's
unsaved work on the canvas.

#211 — deleting the only board made every other component
unresponsive (wires still worked)
SimulatorCanvas.tsx::interactionRunning: the old expression
treated boards.length === 0 as "boardless electrical mode is
running" — which suppressed the property dialog on click and made
non-sensor components look frozen. Fixed by also requiring
useElectricalStore.submittedNetlist !== '' before flipping to the
boardless-running branch. SPICE has to have actually solved at
least once for the mode to engage.

#212 — ESP32 Support 404 with no actionable message
desktop/Esp32QemuPrompt.tsx: catch the raw "download HTTP 404" /
"not found" upstream error and reword it to "ESP32 support is not
yet available for your platform. The Velxio team is preparing
this build - try again in a few days, or use Arduino/RP2040
boards in the meantime." The real fix is server-side (the velxio
team needs to publish a qemu-xtensa.tar.gz for the user's
platform into the asset bucket and update esp32-qemu/latest.json).
Tracked in project/desktop-agent-v040/ follow-ups.

All five fixes verified with `tsc --noEmit` clean and the existing
25-test vitest suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:06:58 -03:00
David Montero d64eebc200 fix(stop): reset CPU to PC=0 on Stop (real-life power-cycle semantics)
The previous fix preserved display state on Stop so Resume could pick
up the multiplexed frame seamlessly — but that's Pause semantics, not
Stop. On a real Arduino, hitting the physical Stop is cutting power:
the next Run must boot from setup(), not continue at the saved PC.

User report on https://velxio.dev/example/uno-7segment :
  > empieza a contar, le doy stop en el 6, le doy run y sigue desde 6

stopBoard now:
  - calls sim.reset() (was sim.stop()) — CPU back to PC=0
  - calls hardResetPinStates() (was the soft resetPinStates) — clears
    cached states AND notifies listeners so 7-seg / NeoPixel / LCD
    blank out instead of freezing on whatever was lit.

Reset and Stop are now the same cold-boot semantics; Reset still
additionally clears serial output + baud rate. The soft
resetPinStates() helper stays for internal SPICE-classification-only
paths that don't want listener fan-out.
2026-05-26 21:28:41 +02:00
David Montero 858e160e6f chore(sitemap): bump lastmod dates to 2026-05-26 2026-05-26 20:53:24 +02:00
David Montero Crespo 91b0829562 feat(desktop): in-app update toast with auto-check at startup (v0.4.0)
Replaces the native OS-modal update dialog (which blocked the editor
and looked dated) with a non-intrusive bottom-right toast that
appears 30 s after app mount when the Tauri updater finds a newer
release.

State machine:
  idle → no update detected, render nothing
  available → "Update available - Velxio Desktop X.Y.Z" + Install/Later
  downloading → progress bar with "X.X / Y.Y MB (NN%)"
  installing → "Installing X.Y.Z... will restart automatically"
  error → error message + Retry/Dismiss

Click "Install and restart":
  1. downloadAndInstall() streams the full signed installer (~70 MB)
  2. Tauri verifies the minisign sig against the embedded pubkey
  3. Replaces the install in-place
  4. Auto-relaunch (the app exits and reopens on the new version)

"Later" dismisses for the rest of the session (sessionStorage flag).
A close+reopen re-checks. Manual re-check via the menu still works.

Companion change in velxio-prod flips tauri.conf.json
updater.dialog from true to false so our custom toast is the only
update UI - no double-prompting.

Files:
- frontend/src/desktop/UpdateAvailableToast.tsx (new): the component
- frontend/src/desktop/desktop.css: toast styles + slide-in animation
- frontend/src/desktop/index.ts: mount alongside GraceBanner +
  Esp32QemuPrompt in the existing sidePanelRoot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:46:58 -03:00
David Montero 5ca7e293b7 fix(canvas): use wrapper top-left for rotation pivot (#205 follow-up)
The previous fix rotated overlay hotspots but the pivot was off by
+6px in each axis, which manifested as a 12px X-offset for a 90°
rotation (because (I - R) maps (6,6) to (12, 0) for R = 90° CW).

The wrapper top-left in container-local coords is -wrapperOffsetX,
not -(6 - wrapperOffsetX). The container origin already sits INSIDE
the wrapper's padding+border by exactly wrapperOffsetX/Y; we have
to back out by that same amount, not by 6 - that amount.

Visual verification on https://velxio.dev/example/esp32-pwm-led-rgb:
overlay centers of rotated resistor now match the rotated pin tips
exactly (was 12px off in X).
2026-05-26 20:42:27 +02:00
David Montero 0a69c9facf fix(canvas): rotate pin overlay hotspots with the component (#205)
Reporter on GitHub: after rotating a component the WIRES followed
the pin tips (already fixed in the (6,6) offset commit) but the
clickable connection boxes stayed in the unrotated layout —
visible misalignment between the rotated component and its
hotspots, no way to start a fresh wire from a rotated pin.

Root cause: PinOverlay renders as a SIBLING of the DynamicComponent
wrapper, not as a child. CSS rotation on the wrapper doesn't reach
the overlay div, so its child pin boxes stay at the unrotated
(pin.x, pin.y) coordinates.

Fix:

- Plumb component.properties.rotation from SimulatorCanvas into
  PinOverlay as a new `rotation` prop.
- In PinOverlay, capture wrapper.offsetWidth/Height when reading
  pinInfo and apply the same rotation matrix the wire calculator
  uses (pivot at wrapper center, transform-origin: center center).
- Use the rotated (pinX, pinY) for both the visual `left/top` AND
  the canvas-coord passed to onPinClick, so wires that get started
  from the hotspot anchor at the rotated tip too.

Also align the default wrapperOffsetX from 4 to 6 (padding:4 +
border:2 on each side of the DynamicComponent wrapper). The
previous asymmetric (4, 6) was the same 2px X bias we fixed in
pinPositionCalculator a few commits back; the overlay was reading
its own copy of the bad number and putting hotspots 2 px left of
the pin tip on unrotated components too. Board paths that pass
wrapperOffsetX/Y = 0 explicitly are unaffected.

All 29 vitest tests in the rotation + simulator suites pass.
2026-05-26 20:02:10 +02:00
David Montero Crespo d347cac51e test(vitest): allow tests outside frontend/ + discover velxio-prod pro overlay tests
Two related changes for the v0.4.0 desktop-agent rollout:

- include glob now also matches `../../pro/frontend/src/pro/**/__tests__/`
  so the agent-overlay tests in velxio-prod are discovered when this
  config is used from a velxio-prod checkout. On pure-OSS clones the
  glob has nothing to match - harmless.

- server.fs.allow extended to `..` and `../..` so Vite's filesystem
  sandbox doesn't reject the cross-project test paths with
  "Cannot find module '/@fs/...'".

No behavior change for OSS-only contributors. velxio-prod gets the
agent's `desktopAuth` unit tests picked up automatically by
`npx vitest run` in this directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:53:04 -03:00
David Montero Crespo 3bd6f144f2 feat(desktop): allow slim pro overlay when VITE_PRO_BUILD + VITE_DESKTOP both set
v0.3.x main.tsx explicitly REFUSED to load @pro when VITE_DESKTOP was
true ("desktop owns its own license + auth UI"). v0.4.0 brings the
AI agent into the desktop bundle, so the refusal needs to relax for
that one use case.

New routing inside the if(VITE_PRO_BUILD) branch:
  - VITE_DESKTOP also set → load @pro/desktop_index (slim entry that
    only mounts AgentChatPanel + DiagnoseCompileButton, no analytics
    / sessions / billing / admin / save overrides - those expect
    velxio.dev cookies the desktop has no way to send)
  - VITE_DESKTOP not set → load @pro/index (existing web behavior)

VITE_DESKTOP alone (no pro) still loads zero overlay - that's the
pure-OSS desktop build path for self-hosters who don't have the
pro source tree at $PRO_OVERLAY_PATH.

Companion commit in velxio-prod creates @pro/desktop_index, adapts
the agent client for license-key Bearer auth, and updates
build-frontend.sh to pass both flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:09:22 -03:00
David Montero b2474bf5d1 fix(stop): preserve display state on Stop, only blank on Reset
Reporter feedback after 7aca3db: pressing Stop on the uno-7segment
example turned the 7-segment off, and pressing Start again left
random segments lit / no number at all. The previous fix made
resetPinStates() notify every listener with (pin, false) on both
Stop and Reset, which was right for Reset (full reboot) but wrong
for Stop:

  - On Stop the AVR CPU is just paused. Internally it still has
    PORTD=0xFF (or whatever the last drive was).
  - resetPinStates blanked the pinStates cache + fan-out LOW
    notifications. Display turns off, fine.
  - On Start the CPU resumes from where it paused. avr8js's port
    listener fires only for bits that CHANGED relative to its OWN
    oldValue (which still holds the pre-stop value). If oldValue
    matches the live register, no pinChange event fires for that
    bit, and the display has no signal telling it to come back on.

Split the API into two methods:

  resetPinStates()    — soft cleanup, drops outputPins only. Used by
                        stopBoard. Cached pinStates and visual state
                        stay so the resume picks up where it left off.

  hardResetPinStates() — full cleanup, drops outputPins + pinStates
                        and fan-outs (pin, false) to listeners.
                        Used by resetBoard (CPU starts at PC=0,
                        firmware re-drives every pin from setup()).

Updated the test helper clearAllPinManagerState to call
hardResetPinStates between tests so the same-state short-circuit in
triggerPinChange doesn't suppress fresh events.

All 32 vitest tests pass (AVRSimulator, interconnect-routing,
dual-arduino-software-serial, pin-position-rotation).
2026-05-26 18:54:17 +02:00
David Montero 7aca3db51c fix(reset): clear display state + don't clobber Interconnect on Reset
Two paired bugs that surfaced on the Reset button.

(1) 7-segment / NeoPixel / LCD freeze on last pattern after Reset.
    resetPinStates() was wiping the pinStates cache + outputPins set
    silently — no listener notifications fired, so visual components
    that update on pinChange kept rendering whatever segments were
    lit at the instant the user pressed Reset. Now we snapshot every
    pin that was HIGH before clearing and fan out a synthetic
    (pin, false) to each registered listener. Stateful displays
    redraw cleanly to all-off; passive listeners (analog sensors,
    debounce-only buttons) ignore the synthetic LOW and recover on
    their next real write.

(2) Cross-board serial silently dies after pressing Reset. resetBoard
    was unconditionally reassigning:
        sim.onSerialData = (ch) => appendSerial(boardId, ch);
    immediately after sim.reset(). The comment said "re-wire after
    reset" but reset() does NOT clear that property — the new USART's
    onByteTransmit chains through `this.onSerialData` which IS the
    Interconnect wrapper. The reassignment destroyed that wrapper and
    sibling-board UART forwarding (Uno TX → Nano RX) stopped working
    until a full page reload. Same root pattern as the initSimulator
    bug fixed in 5480052 — Interconnect's __icSerialHookInstalled
    flag is on the live sim, so once the wrapper is blown away
    nothing reinstalls it. Removed the reassignment and left a NOTE
    so the next person doesn't reintroduce it.

Verified the AVRSimulator + dual-arduino-software-serial +
interconnect-routing test suites still pass (26 tests).
2026-05-26 17:30:44 +02:00
David Montero 5480052379 fix(multi-board): initSimulator wiped Interconnect's UART wrapper
Cross-board UART forwarding silently broke for any project loaded
with > 1 board. User report: Arduino Uno → Arduino Nano serial echo
test where the Uno transmits fine but the Nano's Serial.available()
is never true.

Root cause traced live with chrome-devtools-mcp + temporary debug
logs in AVRSimulator.onSerialData setter and Interconnect:

  1. loadProjectState → addBoard(uno) → createSimulator → sim.onSerialData = appendSerial
  2. addBoard(nano) → same
  3. setWires → Interconnect.updateWires → ensureSerialHook(uno)
     wraps sim.onSerialData with a fan-out callback that ALSO pushes
     to the Nano's RX queue. __icSerialHookInstalled flag set.
  4. SimulatorCanvas mounts → useEffect calls store.initSimulator()
  5. initSimulator unconditionally did:
        simulatorMap.delete(boardId);
        const sim = createSimulator(...);   // ← brand-new sim
        simulatorMap.set(boardId, sim);     // ← Interconnect's wrapper is gone
     The new sim's onSerialData is just appendSerial. The old sim
     (where the wrapper lived) has been orphaned; Interconnect never
     re-installs because its flag was on the discarded sim.
  6. Run all boards → Uno.usart.onByteTransmit → this.onSerialData →
     appendSerial (Uno's monitor shows TX) but no fan-out call →
     Nano never receives anything.

initSimulator is a legacy single-board helper from the days when the
store only knew about one MCU. Multi-board flows already create
their sims in addBoard. Bail out early if a sim for the active
boardId already exists, so the legacy helper becomes a no-op when
the multi-board path has already done the work.

Verified the 3 related test suites still pass (AVRSimulator,
dual-arduino-software-serial, interconnect-routing).
2026-05-26 17:08:04 +02:00
David Montero 1663236184 debug: trace onSerialData setter 2026-05-26 16:57:37 +02:00
David Montero c8d06c9b90 debug: temp console.log in cross-board UART path 2026-05-26 16:47:13 +02:00
David Montero 56d3bc4f12 fix(avr/serial): drain RX queue every frame and clear it on stop
User report: Arduino Nano connected to an Uno-TX wire received bytes
but displayed them poorly, and pressing Stop then Run "killed" the
serial link until the page reloaded.

Two paired bugs in the cross-board serial path:

(1) drainSerialRxQueue was only ever re-fired from usart.onRxComplete,
    which itself only fires AFTER a successful delivery. If the very
    first delivery attempt fails (rxEnable=false because the sketch
    hasn't reached Serial.begin yet — extremely common when one board
    starts emitting bytes before the receiving board's setup() runs)
    nothing re-kicks the queue and every subsequent byte from the
    sibling board sits in serialRxQueue indefinitely. Adding a
    per-frame drain attempt (no-op when queue is empty or rxBusyValue
    is set, so cost is negligible) makes the link self-heal across
    cold-start races and Serial.end()/begin() toggles.

(2) stop() never cleared serialRxQueue. On Run after Stop the new
    USART would re-drain the previous run's leftovers into the fresh
    sketch before its setup() ran, corrupting the first bytes the
    user saw on the receiving side. Clearing the queue in stop() —
    same place we already clear scheduledPinChanges — keeps each Run
    a clean slate.

Verified 52 existing tests still pass (dual-pico-serial-passthrough,
dual-arduino-software-serial, interconnect-routing, avr-uart-tx
-waveform, serial-batching, AVRSimulator, pin-position-rotation).
2026-05-26 15:47:55 +02:00
David Montero 467ca4455f fix(canvas): wires off pins after rotation — wrapper offset was (4,6) instead of (6,6)
User report: "rotating components messes up their connections" — pressing R
on a placed component visibly slid every wire endpoint off its pin tip.

Root cause: the DynamicComponent wrapper has padding:4px + border:2px on
EVERY side, so the inner web-component element sits 6 px in from the
wrapper top-left on BOTH axes. The wire layer assumed an asymmetric
(4, 6) offset, baked into:

  * useSimulatorStore.updateWirePositions       — store.x + 4, store.y + 6
  * useSimulatorStore.recalculateAllWirePositions
      — start (startComp.x + 4, startComp.y + 6)
      — end   (endComp.x   + 4, endComp.y   + 6)
  * pinPositionCalculator.calculatePinPosition  — inverse: (componentX - 4, componentY - 6)

Unrotated the 2 px X bias was visible only as a very-slightly-off wire,
which nobody filed. When the user rotated the component, the bias
rotated WITH it — at 90° it became a 2 px Y offset (wires hanging below
the pin), at 180° a 2 px X offset on the other side, at 270° upward. UX
read as "wires disconnected".

Verified the real CSS box via chrome-devtools-mcp against several live
components on velxio.dev (RGB LED + 3 resistors + analog joystick): all
report padding-left/top = 4 px, border-left/top = 2 px, inner offset = 6
on both axes.

Fix: use (+6, +6) at every site, single source of truth in a comment
explaining padding+border arithmetic. Updated the rotation regression
test to match the corrected math (numbers shift by 2 px on every
expectation that referenced the old offset).

Pin position math, pivot derivation and the rotate-N×90° round trip
unchanged — only the offset constant moved.
2026-05-26 15:16:04 +02:00
David Montero Crespo 161335a5cf test(desktop): unit tests for bannerFor + suppress redundant banner in lockout
Phase 4 polish: GraceBanner was rendering for state=locked/tampered
even though LockoutOverlay covers the screen for those states. The
banner leaked through the overlay's 96%-opaque background as a
faint red strip - confusing.

- GraceBanner.tsx: bannerFor() returns null for locked/tampered
  (LockoutOverlay handles the messaging). Also exported bannerFor
  so the new unit tests can exercise the pure decision logic.
- __tests__/GraceBanner.test.ts (new): 13 vitest cases covering
  pre-expiry amber/red thresholds (trial_ends_at vs subscription_period_end),
  fallback to claims.exp for legacy JWTs, soft/hard grace messaging,
  dismissibility rules.
- vitest.config.ts: include also matches src/**/__tests__/ so the
  desktop tests are discovered without moving them.

Runtime ~600ms vs 5-25 min for a full installer rebuild - lets
future iterations on the banner state machine skip the build cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:05:39 -03:00
David Montero Crespo 6f63fceb20 feat(desktop): paywall v0.3.0 - LockoutOverlay + staged expiry banners
Adds the frontend half of the v0.3.0 desktop paid model. The Tauri
shell (in velxio-prod) emits velxio://license-required when the
license gate refuses to spawn the sidecar; this commit teaches the
OSS desktop overlay to react.

- LockoutOverlay.tsx (new): full-screen modal with three variants
  (no_credential / tampered / expired). Sign-in or paste-key
  resolves it via restartApp().
- DesktopWelcomePage.tsx: new grandfather variant - "you have N
  days to keep using Velxio Desktop" + "Continue without signing in".
- GraceBanner.tsx: rewrite with pre-expiry tones (5d amber, 24h
  red, dismissible), polling every 10 min while document visible,
  separates pre/post-expiry messaging.
- Esp32QemuPrompt.tsx: signup gate for grandfather users (ESP32
  binaries are not part of the grandfather grace) + inline progress
  bar driven by velxio://esp32-qemu-progress events.
- index.ts: rewires on getGateInfo() at first paint to decide
  welcome vs lockout vs nothing; installs license-required listener
  + 10-min foreground polling for the locked transition.
- tauriBridge.ts: adds GateInfo type, getGateInfo(), restartApp().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:16:48 -03:00
David Montero 9e9a3e7800 fix(esp32): use JS template literals (backticks) for stub interpolation 2026-05-25 00:17:47 +02:00
David Montero 8a89322808 feat(esp32): smart WiFi/HTTP stubs so MicroPython examples actually work
Phase 7.7 follow-up. Previously the WiFi stub returned wlan.isconnected()=False
and ntptime.settime() raised OSError — sketches degraded gracefully but
features like the TIME and WEATHER screens in the smart-ui-eyes example
showed "Sync Failed" / "API Error" instead of real-looking data.

Smart stub now:
- wlan.isconnected() returns True after the first ~2 calls (simulates a
  ~1 second connection ramp)
- ntptime.settime() pre-loads machine.RTC() with the host's UTC datetime
  (captured at code-injection time), so localtime() returns real time
- urequests.get(url) returns a stubbed Response whose .json() decodes a
  payload routed by URL substring:
    "openweathermap"/"weather" → fake weather dict (temp/humidity/desc)
    "ipify"/"myip"             → fake public IP
    "worldtimeapi"             → fake ISO datetime
    everything else            → {}
- urequests.post/head also stubbed (return {"ok": True} / {})
- Both `urequests` and `requests` aliases registered

End result: smart-ui-eyes example shows real-looking time on TIME
screen and plausible weather data on WEATHER screen, no crashes.
Still no real internet (would need Phase 7 QEMU WiFi emulation), but
visually the example demos correctly.
2026-05-25 00:15:47 +02:00
David Montero 835ca6d7a8 fix(esp32): stub network + ntptime modules for MicroPython in QEMU
Inject a compat shim into the raw-REPL prelude that replaces
sys.modules["network"] and sys.modules["ntptime"] with no-op stubs
BEFORE user main.py runs.

Why: the picsimlab QEMU fork's esp32_wifi NIC emulation handles
Arduino's lightweight WiFi.h but not MicroPython's full esp_wifi_init
path. Calling network.WLAN(STA_IF) (which is what every
network-using MP sketch does) drives the firmware to wait on
peripheral status bits QEMU never sets, eventually tripping the
FreeRTOS task watchdog (TG1WDT_SYS_RESET ~26s after boot, or
TG0WDT ~14s if the NIC is partially attached).

With the stub:
  network.WLAN(STA_IF).isconnected() -> False
  network.WLAN(STA_IF).connect(...)  -> no-op
  ntptime.settime()                   -> raises OSError

Sketches that already have try/except around sync_time (which is
most of the 100-days examples) now degrade gracefully: WELCOME +
EYES screens run, TIME and WEATHER screens show their fallback
behaviour, no panic, no reboot.

Doesn't affect Arduino C++ — sketches that #include <WiFi.h> use
real WiFi.begin() and the existing esp32_wifi NIC handles those fine.

A proper fix is to extend the picsimlab WiFi emulation to support
the full ESP-IDF API, but that's a multi-day project. This stub
unblocks the 31 MicroPython examples shipping with network imports.
2026-05-23 23:13:20 +02:00
davidmonterocrespo24 e4ecefe46a feat(desktop): skip welcome screen, robust openExternal, in-app nav
Three coordinated changes that fix the "Waiting for browser…" hang
and unblock first-launch UX on the Tauri desktop build:

  1. desktop/index.ts — DON'T mountWelcome unconditionally on first
     launch. Before, an empty keychain (no key yet) forced the
     welcome / sign-in screen on top of the editor, gating 100% of
     the app behind an account. Now the editor opens directly:
     compile + run + sim + save .vlx all work for free (they're
     upstream OSS features), and the license check still runs in
     the background just to populate state for the GraceBanner
     (which shows for invalid keys — locked, tampered, in
     soft/hard grace). Pro-only features (ESP32 QEMU download,
     agent IA) prompt for license at use time, where it actually
     matters. Matches the "try before you buy" expectation a
     desktop install creates.

  2. desktop/tauriBridge.ts — rewrite `openExternal` to try every
     known IPC path in cascade order and log via the desktop debug
     file which one worked. The previous implementation invoked
     `plugin:shell|open` with `{ path: url }`, which silently
     failed (no ACL match + wrong arg shape) and fell back to
     `window.open`, which inside a Tauri webview is a no-op for
     external URLs — the browser never opened. New cascade:
     plugin:opener|open_url (paired with tauri-plugin-opener which
     ships in this revision), then plugin:shell|open with both
     `{ path, with: null }` and `{ url }` shapes, then the
     window.__TAURI__.shell / opener high-level wrappers that
     specific Tauri 2.x flag combos expose. Each attempt logged
     via the dlog helper so the next operator can see exactly
     which path was used (or that all failed) without devtools.

  3. desktop/menu.ts — new `navigate-route` action type. Routes
     bundled in the SPA (DocsPage, ExamplesPage, AboutPage) that
     used to open velxio.dev in the system browser now navigate
     in-window via history.pushState + popstate (mirrors the
     locale-switch handler). Respects the current locale prefix
     so `/examples` from `/es/editor` lands at `/es/examples`
     instead of jumping back to English.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:57:49 -03:00
David Montero cf28b3b5ea fix(esp32): auto-enable WiFi NIC for MicroPython sketches using network module
The hasWifi auto-detection in useSimulatorStore.startBoard only matched
Arduino C++ patterns (#include <WiFi.h>, WiFi.begin). MicroPython
sketches that call `import network` or `network.WLAN(STA_IF)` were
not detected, so wifi_enabled stayed false and the backend never
attached the esp32_wifi NIC model to QEMU.

Symptom: any MicroPython ESP32 example that touches the network
module hangs in network.WLAN(STA_IF) (the constructor that triggers
esp_wifi_init internally) and the FreeRTOS task watchdog trips with
TG1WDT_SYS_RESET ~26 seconds after boot. The chip then reboot-loops.

Mirror the Pico W detector right below this one — it already handles
both Arduino and MicroPython patterns. Now ESP32 does too.

Affects 31 examples in examples-100-days.ts that use network.WLAN.
2026-05-23 22:54:10 +02:00
David Montero b09c8339ac fix(examples): use hardware I2C (not SoftI2C) in smart-ui-eyes
OSError: [Errno 19] ENODEV at ssd1306.SSD1306_I2C(...) on
100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython.

MicroPython SoftI2C bit-bangs GPIO directly. Velxio's ESP32 QEMU
bridge listens on the emulated I2C peripheral (registers slaves like
0x3C wokwi-ssd1306 against it) and doesn't decode bit-banged GPIO
toggles as I2C frames, so the OLED never sees any writes and
i2c.writeto() returns ENODEV on first use.

machine.I2C(0, ...) routes through the hardware I2C peripheral that
QEMU emulates, the registered slave receives the bytes, the OLED
panel updates. Same code path the other working SSD1306 MicroPython
examples on this repo already use.

API surface is identical to SoftI2C — only the constructor differs —
so the rest of the user sketch needs zero changes.
2026-05-23 18:06:42 +02:00
David Montero 76f6cd9a37 fix(avr/serial): queue RX bytes so Serial.readStringUntil sees the whole input
avr8js's usart.writeByte(value) rejects the call (returns false, drops
the byte) whenever rxBusyValue is set — and rxBusyValue stays true for
one full cyclesPerChar after each accepted call. The old serialWrite()
fed every character in a synchronous for-loop, so only the first byte
made it through and the sketch saw 'h' when the user typed 'hello\n'.

Buffer pending bytes in serialRxQueue and pump them one at a time:
- serialWrite() now just queues + kicks drainSerialRxQueue once
- drainSerialRxQueue calls writeByte on the head of the queue and only
  shifts it off if writeByte returned true (avr8js accepted it)
- usart.onRxComplete is wired to drainSerialRxQueue so the next byte
  ships as soon as the sketch's RX side actually consumed the previous
  one — matches the cyclesPerChar pacing the real chip enforces

Same handler wired in both USART setup paths (the Uno/Nano branch and
the post-loadHex Mega/ATtiny branch). TX path (onByteTransmit +
emitUartTxFrame for the oscilloscope waveform) is unchanged.
2026-05-23 17:50:19 +02:00
David Montero 0d43c5f892 fix: relax -Werror for user sketches + un-nest /* */ in robot-desktop-eyes
Two related fixes for the ESP32 Arduino-compat compile path:

(a) backend/app/services/esp-idf-template/main/CMakeLists.txt:
    Demote -Werror=comment / =parentheses / =sign-compare / =narrowing
    / =write-strings / =missing-field-initializers / =reorder back to
    plain warnings. ESP-IDF's project defaults are stricter than what
    Arduino/arduino-cli users expect, so common Arduino idioms (nested
    /* */, missing field initializers in struct literals, etc.) were
    failing builds that compile fine in the Arduino IDE. -Wall stays
    on; we just stop the abort.

(b) examples-robot-desktop.ts (robot-desktop-eyes example):
    Replace the nested /* xTaskCreatePinnedToCore( ... /* Task function. */
    ... */ block with `#if 0 / #endif` so the inner block comments
    don't terminate the outer one. Even with -Wno-error=comment the
    real-syntax-level issue (the first inner `*/` closes the outer
    comment, leaving the rest of the lines as bare code) would still
    bite, so this needs an actual code fix.
2026-05-23 16:19:56 +02:00
davidmonterocrespo24 a86a0a45bd feat(android): Digital Asset Links for TWA verification
Drops `/.well-known/assetlinks.json` so the Trusted Web Activity APK
(dev.velxio.twa, generated by bubblewrap from this same manifest.webmanifest)
can prove to Chrome that it's allowed to claim velxio.dev as its own
origin. Without this file the TWA falls back to a Custom Tab with the
URL bar visible — losing the whole "feels native" UX that TWAs
exist for.

The sha256_cert_fingerprints entry pins the production signing key
held locally as android.keystore in the velxio-twa/ build dir (NOT
in any repo). If we ever lose that key + need to re-issue, this
file has to be updated with the new fingerprint and re-deployed
BEFORE the new APK reaches users; otherwise their previously-
installed TWA verifies against an asset link that no longer matches
the APK signature and breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:42:56 -03:00
David Montero 1a6e2a23a7 fix(examples): auto-install libs for robot-desktop-eyes
Sketch fails to compile out of the box with
  fatal error: ESP32Servo.h: No such file or directory
because the ESP32Servo / U8g2 / DHT / Adafruit Unified Sensor libs
aren't part of arduino-esp32 and weren't declared on the example.

loadExample.ts already iterates `example.libraries` and runs
arduino-cli lib install for any missing entry before the user
touches Compile. Adding the four real deps the sketch needs gets
the example compiling cleanly on a fresh container without any
manual Library Manager dance.
2026-05-23 08:49:26 +02:00
David Montero 2f60e3816a fix(editor): surface QEMU compile failure to the user instead of silent warning
When the auto-compile path in handleRun() finishes without producing a
compiledProgram, the previous code dropped the failure on the floor with
only a `console.warn` — the user clicked Run, nothing happened, and they
had no idea why. The accompanying comment also promised "always start
even if compiledProgram is empty" but the code did the opposite.

This commit replaces the dead comment + silent warn with a top-level
error toast + addLog entry, with a different copy for MicroPython mode
(suggests "click Load MicroPython to retry") vs Arduino C++ mode
(directs the user to the output console for the underlying error).

handleCompile already writes the actual cause to the compile-output
console via addLog — this fix just makes sure the user knows their
click failed and where to look.
2026-05-23 08:39:26 +02:00
davidmonterocrespo24 e91aaea2fd chore(manifest): make PWA mobile-friendly + better TWA copy
Three small fixes to frontend/public/manifest.webmanifest so the
Bubblewrap-generated TWA (and Add-to-Home-Screen PWA installs) feel
right on a phone:

  - orientation: landscape → any. Landscape-forced on a phone
    locks the device in side-grip whenever Velxio is foregrounded;
    the editor + simulator work fine in portrait too (the file
    explorer collapses gracefully). Tablets and desktops still
    default to landscape because they're naturally wider, so this
    only changes behaviour where the lock would actively hurt.
  - name: "Arduino Emulator" → "Circuit & Arduino Simulator".
    Matches the title tag + Open Graph copy that velxio.dev uses
    everywhere else and reflects the SPICE / ESP32 / RP2040 work
    the project has grown into since the original name was written.
  - description: was 'Free local Arduino emulator … No cloud, no
    latency.' That was true for the OSS self-host but misleading
    for an installed PWA that talks to velxio.dev. Rewritten to
    describe the actual product surface (the boards, the SPICE
    sim, "free and open source") without making a claim the live
    site can't keep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 03:38:31 -03:00
David Montero dbab794ddb test(snapshots): refresh smart-ui-eyes netlist after circuit was wired
Snapshot was written when the example had components: [] and wires:
[]. Now that 5bd541e populated the circuit (OLED + 2 buttons) and
2dea3f0 renamed the OLED pins to match wokwi-ssd1306's real
pinInfo, the netlist contains the button pull-down resistors and
floating-net autopulls. Regenerated with `vitest run -u`.
2026-05-23 08:28:08 +02:00
David Montero 2dea3f0448 fix(examples): correct SSD1306 + big-sound-sensor pin names
robot-desktop-eyes and the day-30/100-days OLED example wired the
SSD1306 OLED with SDA/SCL/VCC, but the wokwi-ssd1306 element
exposes pinInfo as DATA/CLK/VIN/GND. The mismatched names couldn't
resolve, so all three wire endpoints fell back to (0,0) of the
component and visually attached to the corner instead of the pins.

Same class of bug on the wokwi-big-sound-sensor in
robot-desktop-eyes: the element has AOUT/DOUT (no plain OUT). The
sketch uses digitalRead(SOUND_PIN), so route to DOUT.

The COMPONENT_PIN_ALIASES map in wokwiZip.ts only normalises on
.zip import — static examples have to use the real pinInfo names.
2026-05-23 08:23:15 +02:00
David Montero 5bd541e56f fix(examples): add OLED + buttons circuit to ESP32 smart-ui-eyes example
The 100d-esp32-oled-smart-ui-eyes-animation-time-and-weather-micropython
example had components: [] and wires: [] — the MicroPython code wired
an SSD1306 OLED on I2C (GPIO 21/22) plus two buttons (GPIO 14, 27) but
the circuit had nothing on the canvas, so users saw a bare ESP32 board
and the simulation was missing every peripheral the code drives.

Adds:
- wokwi-ssd1306 on I2C (3V3 / GND / SDA=21 / SCL=22)
- two wokwi-pushbuttons wired HIGH-when-pressed (3V3 → 1.l, 2.l → GPIO
  14 / 27) to match the `if pin.value(): pressed` check in main.py
2026-05-23 08:16:13 +02:00