Commit Graph

11 Commits

Author SHA1 Message Date
David Montero eb6a59adf6 style(ui): unify every interactive surface on the single #0071e3 accent
Sweep all off-brand blues (#007acc logo-blue, #0e639c, #3b82f6) and the neon
cyan family (#4fc3f7, #00b8d4, #00e5ff, rgba 0,184,212 / 0,122,204) across the
editor, simulator, examples gallery and pages to var(--color-action-primary).
The Code/Both/Circuit toggle, board chips, library manager, modals etc. now
match the Libraries / +Add buttons. Also keep the editor Libraries label
visible in the default split (lower the container-query threshold).
2026-06-21 05:34:25 +02:00
David Montero Crespo ca8dcedcc7 feat: STM32 (Blue Pill / Black Pill) QEMU emulation + Pro board gating
STM32 emulation (open-core, runs via libqemu-arm in the backend worker):
- backend: stm32_lib_manager + stm32_worker (GPIO, USART, I2C/SPI device models
  reusing the ESP32 slaves, live sensor updates), arduino_cli STM32 branch,
  start_stm32 simulation route.
- frontend: Stm32Bridge + Stm32BluePill(/BlackPill) web components (Wokwi SVGs),
  board kinds, Interconnect/boardPinMapping/boardProtocols wiring, example
  projects (blink, serial, I2C BMP280/MPU6050/DS1307/SSD1306/weather, 7-seg,
  RGB, button, switch, stepper, cross-board interconnect).
- Raspberry Pi 4/5 board elements + thumbnails.

Pro board gating (generic OSS->Pro seam; entitlement logic lives in the overlay):
- lib/proBoardGate.ts: isProBoardKind (STM32 + every QEMU Raspberry Pi),
  installBoardGateImpl/boardGateDecision, triggerProUpgradePrompt.
- PRO badge on those boards in the component picker; gate at the picker add +
  the run backstop (startBoard).
- backend/app/services/board_access.py: server-side enforcement seam for the
  simulation WebSocket; STM32/Pi unavailable -> Pro-framed message.
- desktop: generic QemuDownloadPrompt + Stm32QemuPrompt (download-behind-license,
  mirrors the ESP32 prompt).
- .gitignore: never ship libqemu-* binaries in the public image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:06:14 -03: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 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 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
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
davidmonterocrespo24 2146b09c29 feat(desktop): hide header strip, splash screen, native locale switcher
Three QoL fixes for the Tauri shell:

  1. Hide the entire AppHeader strip in VITE_DESKTOP, not just the
     marketing nav. The previous gate left the black bar painting
     over the editor with the brand + auto-save + share + auth
     slot, all of which are irrelevant in desktop (cloud Pro
     features, license is handled by DesktopWelcomePage, the title
     bar already says "Velxio Desktop"). Return null at the top so
     the editor takes the full window height.

  2. Splash screen during sidecar boot + Monaco hydration. Cold
     launch was a 3-8 s black window — now there's an inline SVG
     logo, "Velxio" wordmark, slogan, animated spinner, and a
     "Starting local backend…" caption. Lives in index.html as a
     fixed-position overlay with display:none by default; the inline
     script reveals it only when `window.__TAURI__` is present, so
     web users never see it. main.tsx fades it out (250 ms ease-out)
     after two animation frames — guarantees React's first paint has
     committed before the handoff, no black flash. Self-contained:
     inline styles, inline SVG, inline CSS keyframes, zero external
     requests.

  3. Native locale switcher under View → Language. Emits
     `velxio://menu` with action='set-locale' + the locale code; the
     desktop/menu.ts handler navigates via history.pushState +
     popstate so React Router picks it up without a hard reload
     (Monaco + simulator state preserved). Locale list mirrors
     i18n/config.ts::LOCALES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 19:39:22 -03:00
davidmonterocrespo24 30d0da430b feat(desktop): native menubar bridge + best-effort file logger
Two new modules under the existing desktop/ subtree, both no-op
outside a Tauri runtime (tauriBridge.listen / .invoke fail gracefully).

  desktop/menu.ts — listens for the `velxio://menu` event the Rust
    shell emits from the native menubar (Velxio Desktop / File / Edit
    / View / Help, full menu defined in
    pro/desktop/src-tauri/src/menu.rs). Internal actions handled
    directly here: Save .vlx and Open .vlx via utils/vlxFile, Toggle
    Serial Monitor via useSimulatorStore, Check for Updates via the
    tauri-plugin-updater global. The rest (new-project, Find,
    Toggle File Explorer) re-emit as window CustomEvent so the owners
    of that UI state can subscribe without pulling this module in.

  desktop/log.ts — `dlog(message, extra?)` round-trips a line to a
    Rust `write_debug_log` command that appends to
    `<app_data_dir>/desktop-debug.log`. Packaged Tauri apps have no
    devtools or stdout capture, so this is the only way to see what
    the webview did when a user reports a bug. Falls back to plain
    console.log when the command isn't registered (older shell).

mountDesktop() now installs the menu listener and dlog's its own
start — useful as a "did the desktop module even load" smoke marker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:08:46 -03:00
davidmonterocrespo24 04d93d4f46 feat(frontend): desktop overlay (welcome, grace banner, ESP32 prompt)
New `frontend/src/desktop/` module loaded only when VITE_DESKTOP=true.
Hosts the Tauri-bound UI that pure OSS doesn't need:

  - tauriBridge.ts   typed invoke / listen / openExternal / beginSignIn
                     wrappers with no-op fallbacks for `vite dev` outside
                     the Tauri webview.
  - DesktopWelcomePage.tsx   sign-in flow with browser handoff +
                              paste-key fallback; listens for
                              `velxio://auth-completed` from the shell.
  - GraceBanner.tsx          renders soft/hard grace banners driven by
                              `license_status` + `velxio://license-status`
                              emits from the background checkin loop.
                              Toggles `body.vlx-desktop-readonly` so the
                              editor's Save / Compile buttons disable
                              themselves via CSS in hard-grace.
  - Esp32QemuPrompt.tsx      one-time download modal when the user picks
                              an ESP32 board on a fresh install.
  - index.ts                 mounts welcome conditionally and the side
                              panels unconditionally.
  - desktop.css              shared styles.

All entry points are no-ops outside Tauri so the existence of the
folder has zero effect on the OSS build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:50:51 +02:00