Commit Graph

404 Commits

Author SHA1 Message Date
David Montero Crespo 1e4d78fda5 fix(board): raspberry-pi-pico renders a real Pico, not Nano RP2040 Connect
The 'raspberry-pi-pico' boardKind used to render <NanoRP2040> — a
<wokwi-nano-rp2040-connect> Web Component. That's a completely
different board: it has pin labels D2..D13 / A0..A7 / 5V / VIN,
and a horizontal 168×68 layout. The actual Raspberry Pi Pico has
GP0..GP28 / 3V3 / VBUS / VSYS and is vertical-narrow (105×264).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Wires added:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Body of the work is two number changes + two color tokens; the
rest of the component logic (pointer routing, world rendering,
clamping) is untouched.
2026-05-14 22:32:42 +02:00
David Montero Crespo 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
David Montero Crespo a9200da631 fix(seo): drop #root-seo on mount so it stops inflating page scroll
index.html ships a #root-seo div with prerendered SEO content (so crawlers
that don't run JS still index per-route copy). The inline CSS comment said
"React removes it on mount", but nothing actually removed it — so every
page kept a position:absolute, ~4096px-tall, visibility:hidden element
parked at top:0. That element does not paint, but it DOES contribute to
document.documentElement.scrollHeight.

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:52:12 -03:00
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 aaebfedd23 feat(landing): bump pricing display to 100/500/2000 daily credits
Mirrors the velxio-prod backend quota bump (PLANS dict in
pro/backend/app/pro/services/quota.py). With 9router serving the bulk
of agent traffic for free, the cost-of-LLM ceiling is much lower than
when the limits were originally tuned, so we can be significantly more
generous and let casual users actually evaluate the agent.

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:10:25 +02:00
davidmonterocrespo24 888ce03cc3 docs: BUILD-QEMU.md + 'Build QEMU from source' docs section
Adds a transparent self-hosting path for users who would rather not
run Velxio's prebuilt libqemu binaries. The prebuilts have always
been a convenience under the AGPLv3 license; this just documents
how to skip them.

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

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

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

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

Bundled:
  test/pico_doom_demo/arduino_sketch.ino — source of truth sketch
  test/pico_doom_demo/README.md          — what + why + pin map
  test/pico_doom_demo/compile_check.sh   — operator script that
    runs arduino-cli against the same FQBN the prod backend uses
  frontend/src/data/examples.ts          — gallery entry (boardType
    raspberry-pi-pico, category games, difficulty advanced, 5
    components, 14 wires)
  frontend/src/__tests__/examples-pico-doom.test.ts — 10 vitest
    assertions: example is registered exactly once, target board /
    category / difficulty match, libraries declared, all four
    pushbuttons present, every wire endpoint references a real
    component id, SPI pin mapping matches the sketch's #define
    block, every button has a GND wire, the renderFrame loop is
    still present in the embedded code.
2026-05-13 22:54:35 +02:00
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 cd4050c499 feat(landing+theme): multiplier pricing copy + softer canvas
Two small fixes that compound:

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

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

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

All 9 locales updated for (1) and (2).
2026-05-13 21:02:48 +02:00
davidmonterocrespo24 b7797b1eea feat(landing): AI agent + pricing sections
Two new sections on the landing page, between Features and Support:

1. "Powered by AI agents" — three cards explaining the in-editor agent
   (place & wire parts, generate code, diagnose circuits). Calls out
   DeepSeek-V4-Flash + Gemini 2.5 as the LLM backbone so visitors know
   the simulator does more than draw boxes.

2. "Pricing" — three cards summarising Free / Pro / Pro Max with the
   actual monthly cost, the daily AI-message quota, and a CTA per
   tier. Pro is highlighted as Most Popular. Free CTA opens the editor,
   the two paid CTAs link to /pricing where the PayPal subscription
   flow lives.

The simulator itself stays free — only the AI-agent quota changes per
tier — that copy is repeated in the section subtitle so visitors don't
worry about the boards/components becoming paywalled.

All 9 locales translated.
2026-05-13 20:24:40 +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
davidmonterocrespo24 5d40408718 feat(landing): licensing section — AGPLv3 + commercial option
Adds a two-card section at the foot of the landing page (before the
brand footer) that surfaces Velxio's licensing model: AGPLv3 for the
public release, commercial license for teams that need to ship
Velxio inside closed-source products. Mirrors the existing
.feature-card visual language so it slots into the page without a
new design system.

Commercial CTA opens a mailto:info@velxio.dev. Open-source CTA links
to GitHub via the existing trackVisitGitHub handler so the analytics
event still fires.

All 9 locales translated.
2026-05-13 14:27:49 +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 81ac2283c6 fix(canvas): wires follow components on rotation
Rotating a part with the 90° button used to leave every wire pinned to
the pre-rotation pixel coordinates — the component visually unhooked
from its cables. Two paths were missing:

  1. useSimulatorStore.updateComponent only triggered updateWirePositions
     for x/y changes. A rotation went through properties.rotation, so
     wires never recomputed.
  2. calculatePinPosition didn't know about rotation. Even when called,
     it returned the unrotated offset, so the new endpoints would still
     have been wrong.
  3. recordRotate (undo/redo) skipped updateWirePositions on both legs,
     so Ctrl+Z after a rotate left the canvas inconsistent.

Fix:

  - calculatePinPosition gets a 5th `rotation` argument. When non-zero,
    it finds the .dynamic-component-wrapper ancestor in the DOM, reads
    its offsetWidth/Height (layout-only, immune to CSS transforms) to
    locate the wrapper centre, and applies a 2D rotation matrix around
    that pivot. The wrapper top-left is recovered as (componentX - 4,
    componentY - 6) to match the offset convention updateWirePositions
    already uses.
  - updateWirePositions and recalculateAllWirePositions read the per-
    component rotation and thread it through.
  - updateComponent recomputes wires whenever properties.rotation
    changes, mirroring the existing x/y path.
  - recordRotate.execute and .undo both call updateWirePositions so
    Ctrl+Z keeps the canvas coherent.

Tests (pin-position-rotation.test.ts, 6 cases): unrotated identity,
90° (left edge → bottom), 180° (point reflection), 360° round-trip,
negative angles, and a store-level integration that rotates a fake
component and asserts wires[0].start moves to the rotated coordinate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:03:32 -03:00
David Montero Crespo 36ad2bef3f feat(i2c): cross-board bridging across all velxio boards (AVR/RP2040/ESP32 xtensa+riscv)
Closes the remaining gaps in cross-board I2C so any topology of
supported boards (Uno↔ESP32, two ESP32s, Uno↔Uno↔Uno, ESP32-C3
connected to anything, etc.) works end-to-end with all I2C
components including write-only sinks (SSD1306, PCF8574, LCD-I2C).

Implementation (6 phases):

1. **BFS routing in I2CBusManager**: connectToSlave + handleExternalConnect
   walk the bridge graph with a visited Set so multi-hop chains
   (A↔B↔C with the device on C) resolve transparently.  A new
   forwarder-device shim is installed at intermediate hops so the
   existing handleExternalWrite/Read/Stop machinery routes
   through without per-method visited tracking.

2. **Per-peer proxy ownership in Esp32BridgeShim**: replaces the
   global _proxiedAddrs Set with _proxiedByPeer Map so concurrent
   bridges to the same ESP32 (e.g. wired to both Uno and Pico)
   don't wipe each other's proxies on teardown.  Interconnect's
   per-wire teardown calls clearProxiesForPeer(peerBus) instead of
   clearAllProxies.

3. **BFS-aware proxy sync**: syncProxyFromPeer now walks the peer
   bus + its transitive bridges, so an ESP32 sees devices on
   boards two or more hops away.  _peerDeviceLookup keeps a flat
   addr → device map for write-forwarding and resync.

4. **Periodic resync (250 ms)**: Esp32BridgeShim runs a setInterval
   while any proxy is live, re-dumping each device with
   dumpRegisters() and pushing updateProxyI2c only when an XOR-
   stride hash changes.  This keeps RTC time advancing visible to
   ESP32 firmware without flooding the WS pipe with static
   calibration dumps.  Hash is primed during initial sync so the
   first tick doesn't push a redundant identical buffer.

5. **Write-forwarding ProxySlave → peer**: backend ProxySlave
   buffers write bytes during the transaction and emits a
   `proxy_i2c_complete` event on STOP / repeated-START.  Frontend
   Esp32Bridge dispatches the event to a new onProxyI2cComplete
   callback; the shim replays the byte sequence on the actual
   peer I2CDevice via writeByte() + stop().  Makes ESP32 firmware
   writes to peer SSD1306 actually repaint the OLED, peer PCF8574
   latch updates, peer I2CMemoryDevice register mutations propagate.

6. **ESP32-C3 routed as bridge**: Interconnect.isBrowserSim no
   longer claims c3/xiao-c3/c3-supermini — they were already
   going through Esp32Bridge per the store's ESP32_RISCV_KINDS
   routing, but Interconnect was treating them as browser sims
   which broke proxy install.  isEsp32Bridge now correctly
   includes c3 family + ESP32-S3 + Arduino Nano ESP32.

Defensive: addBoard now disposes any existing shim's proxies
before overwriting simulatorMap entry so test reruns don't leak
timers.

Tests:
- 4 BFS multi-hop tests (i2c-multi-board-slave-gap.test.ts)
- 11 cross-board scenarios + per-peer + write-forward + resync
  (i2c-esp32-multiboard-bridge.test.ts)
- 1 real-firmware E2E for write-forward via QEMU (compile +
  load + observe proxy_i2c_complete arriving with the byte)
- New sketch fixture: esp32_i2c_write_to_peer.ino

Result: 90 test files / 1295 tests pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:51:39 -03:00
David Montero Crespo 44789cf58b feat(auth): welcome email on register + password reset via Odoo mail relay
Adds the transactional email pipeline driven from the Odoo SMTP relay so
new sign-ups get a Velxio-branded welcome and existing users can reset a
forgotten password without us running our own outbound mail server.

Backend:
- PasswordResetToken model: one-time, SHA-256-hashed (plain text never on
  disk), TTL 60 min, marked used_at on consume to prevent replay.
- POST /auth/forgot-password — anti-enumeration (always 200 + generic
  message), rate-limited 3/hour/user.
- POST /auth/reset-password — verifies token, hashes new password,
  atomically marks token used.
- /auth/register hooked with asyncio.create_task to fire welcome mail —
  registration is never blocked on Odoo being up.
- New service app/services/odoo_mail.py: async httpx wrapper, fire-and-
  forget, swallows every error so the request lifecycle stays clean.
- Settings ODOO_URL / ODOO_API_KEY / ODOO_MAIL_TIMEOUT_S /
  PASSWORD_RESET_TOKEN_TTL_MINUTES / PASSWORD_RESET_RATE_LIMIT_PER_HOUR.

Frontend:
- /forgot-password page (single email field + "check your inbox" state).
- /reset-password?token=XYZ page (new password + confirmation, redirects
  to /login?reset=ok on success).
- "Forgot your password?" link + green confirmation banner on /login.
- authService gains requestPasswordReset() and resetPassword().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:34:30 -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 e65fa69abd docs(frontend): update README to reflect npm packages instead of third-party clones 2026-05-11 13:03:52 -03:00
David Montero Crespo ac7d74b2b5 fix(monaco): cross-platform postinstall via Node script
Replace Unix-only shell one-liner (mkdir -p / printf / cp -r) with a
Node.js ESM script (scripts/copy-monaco.mjs) that works on Windows,
macOS and Linux alike. The script still writes public/monaco/.gitignore
to keep copied assets out of git.
2026-05-11 12:42:33 -03:00
David Montero Crespo 64024207d8 fix(monaco): ignore public/monaco/ in git per Copilot suggestion (PR #163)
- postinstall now writes a '*' .gitignore into public/monaco/ so the
  copied monaco-editor assets are never tracked as untracked files
- Also add public/monaco/ to frontend/.gitignore as a belt-and-suspenders
  fallback for the same reason
2026-05-11 12:24:45 -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 13789ae08c
Merge pull request #169 from naweiss/fix/wire-color-on-finish
Make wire connected to ground black even when ending in ground
2026-05-11 12:18:32 -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
David Montero Crespo 5c70f09afe
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 11:36:51 -03:00
naweiss f9ed3b3d44 Make wire connected to ground black even when ending in ground 2026-05-11 08:54:56 +03:00
naweiss fff98915f5 Fix wire hovering in desktop mode 2026-05-11 08:41:51 +03:00
naweiss 8d8e763724 Load monaco editor from local installation 2026-05-11 06:26:08 +03:00
davidmonterocrespo24 9c93d99802 fix(micropython-esp32): write helper .py files to flash before main.py
loadMicroPythonProgram only forwarded main.py (or files[0]) to the
bridge for raw-paste injection. Any auxiliary module the project
imported (mylib.py, drivers, etc.) never reached the device, so
`import mylib` died with ModuleNotFoundError.

Build a Python prelude that writes every other .py file to the
MicroPython filesystem via raw REPL, then runs main.py in the same
paste. JSON.stringify produces an ASCII-safe Python-compatible string
literal for the file body, which keeps the prelude inside the existing
chunked-UART path Esp32Bridge already uses to feed the 128-byte FIFO.

The RP2040 path was already multi-file via sim.loadMicroPython(files),
so it stays untouched.

Reproduces with the project shared in the bug report:
  https://velxio.dev/project/ac7e285c-8dc3-4d51-8751-b4aba9912f9e
2026-05-10 01:42:11 +02: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 1869ace89d test(metadata): drill into components array for BMP280 lookup
components-metadata.json is shaped { version, components: [...] }, not a
flat array. The previous test assumed the latter and crashed on
default.find at module load on master, breaking CI for every PR.
2026-05-09 23:52:36 +02:00
David Montero Crespo a7e796161e fix(i18n): split common.json (editor + about) so all 8 locales translate
The common bundle ballooned to 30KB after Block 15 added the AboutPage
prose, putting Russian translations past DeepSeek's 8192-token output
cap. The editor + about sub-trees (the two heaviest, ~15KB combined)
move to a new common2.json file. Both files now sit at 12-18KB and
translate cleanly.

i18n bootstrap merges common2 into the same common namespace at
load time and lazy-loads it per locale, so every existing t('editor.*')
and t('about.*') call keeps resolving without source changes.

All 9 locales regenerated via DeepSeek. Closes the gap left by the
Blocks 15+16 commit where only zh-cn/common had been refreshed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:50:21 -03:00
David Montero Crespo 3428ab130a feat(i18n): translate AboutPage prose + 15 SEO landing pages (blocks 15+16)
AboutPage: ~28 prose blocks across Story / How It Works / Open Source /
Creator / Releases / Quote / Community sections; <Trans> for paragraphs
with inline <strong>, <em>, <a> markup.

15 SEO landing pages now use t() for all user-facing copy under
seo.<page>.* keys: CircuitSimulatorPage, SpiceSimulatorPage,
ElectronicsSimulatorPage, CustomChipSimulatorPage, Attiny85SimulatorPage,
ArduinoSimulatorPage, ArduinoEmulatorPage, AtmegaSimulatorPage,
ArduinoMegaSimulatorPage, Esp32SimulatorPage, Esp32S3SimulatorPage,
Esp32C3SimulatorPage, RaspberryPiPicoSimulatorPage,
RaspberryPiSimulatorPage. Code blocks, FQBNs, JSON-LD schema strings
intentionally stay in English.

The seo bundle (67KB English source) is split into 4 balanced files
(seo.json + seo2.json + seo3.json + seo4.json, ~17KB each) so each
DeepSeek translation request stays inside the 8192-token output cap.
i18n bootstrap merges all 4 halves under the seo.* keyspace.

Translations: 8 locales × 4 seo bundles all regenerated via DeepSeek.
common.json (now 30KB after about additions) only has zh-cn refreshed
so far — the remaining 7 locales' common.json need a follow-up pass
(the bundle is at the edge of DeepSeek's output limit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:38:51 -03: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 7f437a753e feat(i18n): translate DocsPage prose (block 14)
Wire useTranslation() + Trans into DocsPage.tsx. ~330 user-facing
strings across 13 sections (intro, getting-started, emulator, riscv,
esp32, rp2040, rpi3, components, roadmap, architecture, third-party,
mcp, setup) plus sidebar nav + page chrome are now keyed under docs.*.

Strings with inline <a>, <code>, <strong>, <em> use the <Trans/>
component with mapped slots; bare prose uses t().

Code blocks, FQBNs, hex addresses, library names visible as link text,
and JSON-LD schema strings stay in English on purpose.

Internal Link to=... wrapped with localize() so /es/docs/... etc.
keep their locale prefix.

The English docs bundle is split in half (docs.json ~22KB +
docs2.json ~22KB) so each fits inside DeepSeek's 8192-token output
window. The i18n bootstrap merges both halves into the docs.* keyspace
under the default common namespace.

All 9 locales regenerated via DeepSeek (parallel run for the two
namespaces).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:47:19 -03:00
David Montero Crespo 0ff76a0f9d feat(i18n): translate Velxio 2.0 + 2.5 release pages (block 13)
Wire useTranslation() into Velxio2Page and Velxio25Page: hero badge,
accent, subtitle, CTAs, board/example/outcome cards, OSS section, and
footer links all keyed under landing.v2.* and landing.v25.*.

Split en/common.json (34KB) into common.json (25KB) + releases.json
(9KB) so each translation request stays inside DeepSeek's 8192-token
output cap. i18n bootstrap merges both bundles into the default common
namespace at load time, lazy loader fetches both per locale.

translate-i18n.mjs: set max_tokens=8192 + response_format json_object
on the DeepSeek call so future bundles closer to the cap don't get
silently truncated.

All 9 locales regenerated via DeepSeek (fr/de/es/it/pt-br/zh-cn/ja/ru).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:19:16 -03: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 6bc9fe80ce feat(i18n): translate AdminPage + UserProfile + Pricing + EditorPage shell (Editor block 10 — final cleanup)
Closes the Phase 2 i18n rollout. Every visitor- and user-facing
surface velxio renders in normal use now reads from t().

AdminPage (admin-only)
- Header (panel title, logout) and the four tabs (Dashboard /
  Users / Projects / Boards).
- Setup screen for first-admin creation (title, body, password
  fields + mismatch error + create-admin button).
- Not-admin gate page.
- EditUserModal (title, four labels, admin/active toggles,
  cancel/save).
- UsersTab: search placeholder, count pluralisation, all 12
  table columns, Activity / Edit / Delete actions, empty state,
  delete-confirm prompt with username interpolation.
- ProjectsTab: search placeholder, count pluralisation, all 9
  table columns, public/private badge labels, delete action +
  confirm with project-name interpolation, empty state.
- All error messages (load failed / save failed / delete failed)
  fall back through t().

UserProfilePage
- "New project" CTA, loading + empty + not-found states,
  "Private" project badge, "Copy shareable link" tooltip.
- The /editor link uses localize() so /es/<username>'s "New
  project" button stays in Spanish.

PricingPlaceholder
- Title + the two paragraphs (self-hosted note + hosted Pro
  tier note + GitHub source note). Inline links wrapped via
  the Trans component so the link surface stays clickable in
  every locale without each translation having to re-write the
  HTML.

EditorPage shell
- Mobile bottom-tab labels (Code / Circuit), file-explorer
  toggle (Show / Hide), View mode aria-label, view-mode
  segmented control labels (Code / Both / Circuit), and the
  three "Drag to resize" handle tooltips on the panel splitters.

Translations
- en.json hand-curated for the new keys.
- All 8 non-English locales auto-translated via the existing
  `npm run translate:i18n` pipeline (DeepSeek, ~5 min for the
  whole bundle, sameShape() validates each output before write).

This closes Phase 2 of i18n. Phase 3 (DocsPage prose, AboutPage
long-form paragraphs, the 15 SEO landing pages) is deliberately
deferred — Docs/About are best handled by extracting the prose
into JSON keys and running the same script, while the SEO pages
are intentionally optimised for English keyword targeting and
should not be machine-translated en masse.
2026-05-09 12:49:13 -03:00