Commit Graph

662 Commits

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

Two surgical changes:

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

2. SimulatorCanvas.tsx's touch path used to early-return on touchstart
   when interactionRunning + .web-component-container, killing any
   chance of a touch-drag. Now we remember the touch's start position
   in pendingTouchDragRef and let the browser keep synthesizing mouse
   events for the wokwi-element. If the finger drifts past
   DRAG_PROMOTE_THRESHOLD_PX (8 px) onTouchMove cancels the
   passthrough and starts a real component drag — dispatching a
   synthesized mouseup on the original target so the wokwi-element
   doesn't stay visually pressed mid-drag.
2026-05-13 16:51:52 +02:00
David Montero Crespo 3e24811a0d
Merge pull request #174 from davidmonterocrespo24/feat-landing-licensing-section
feat(landing): licensing section — AGPLv3 + commercial option
2026-05-13 09:50:10 -03: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 f3f5d0b6de feat(user): add plan_id column for agent quota tier management 2026-05-13 03:21:51 -03: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 55581690f9
Merge pull request #173 from davidmonterocrespo24/feat-sync-partner-before-mail
feat(odoo-mail): sync partner upsert before firing async mails
2026-05-12 22:38:45 -03:00
David Montero Crespo 248d5ed37b Merge branch 'master' of https://github.com/davidmonterocrespo24/velxio 2026-05-12 22:37:45 -03:00
davidmonterocrespo24 4df7abc624 feat(odoo-mail): sync partner upsert before firing async mails
Closes the register-then-immediately-forgot race for good. Two parallel
calls to /velxio/api/send-welcome and /velxio/api/send-password-reset
were hitting Odoo's REPEATABLE-READ snapshot timing: both workers took
their snapshot BEFORE either had committed the partner row, so each
ran its own INSERT and the second blew up on the velxio_user_id
unique constraint — costing one of the two emails.

Add a new sync_partner() helper that POSTs to a new Odoo route
/velxio/api/upsert-partner (lives in velxio_subscription) which only
upserts the partner — no mail, no subscription, fast. Register and
forgot-password await this BEFORE firing the async welcome / reset
tasks. Each user's flow is sequential at the Velxio HTTP layer, so
the snapshot race disappears.

sync_partner() reuses the existing _post() error-swallowing pattern.
If Odoo is down, sync_partner returns None and the await is a no-op
— the user still registers / gets the generic 200, the welcome /
reset endpoints retain their defensive upsert as a fallback path.
2026-05-13 02:59:52 +02:00
David Montero Crespo 60d570cc5a
Merge pull request #172 from davidmonterocrespo24/fix-odoo-password-reset-include-user-id
fix(odoo-mail): forward velxio_user_id on password-reset payload
2026-05-12 18:33:39 -03:00
davidmonterocrespo24 40edae15b8 fix(odoo-mail): forward velxio_user_id on password-reset payload
Adds velxio_user_id to the send_password_reset payload (mirroring
send_welcome). The Odoo side's res_partner.velxio_user_id is unique,
so when Odoo eventually upserts on this endpoint the constraint
serializes concurrent register-then-immediately-forgot upserts and
prevents the duplicate-partner record the previous wire format risked.
2026-05-12 23:28:57 +02: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 7b760f860a docs: add missing third-party repo references to README 2026-05-11 13:05:21 -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 391df34f5a fix(repo): remove broken submodule entries from third-party/
The 12 repos under third-party/ were tracked as gitlinks (mode 160000)
without a matching .gitmodules file, causing 'git submodule' errors and
broken links in the GitHub UI (issue #164).

These repos are reference-only clones and are NOT required to run Velxio
(npm packages @wokwi/elements, avr8js, rp2040js cover all runtime needs).
Removing them from git tracking and ignoring the directory entirely so:
- New cloners get a clean repo with no broken submodule warnings
- Existing local clones keep their third-party/ folders untouched
- Optional manual clones remain possible for offline hacking

Closes #164
2026-05-11 13:01:13 -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 a785a7e108 Merge remote master (PR #170) - integrate Copilot suggestions
# Conflicts:
#	frontend/src/components/simulator/SelectionActionBar.tsx
2026-05-11 12:17:20 -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 19c09ae260
Merge pull request #170 from naweiss/feat/wire-color-picker
Add color pallete for changing wire color
2026-05-11 12:13:28 -03:00
David Montero Crespo 46f615ef69
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 11:51:03 -03:00
David Montero Crespo 861e22572e
Merge pull request #163 from naweiss/fix/offline-editor
Load monaco editor from local installation
2026-05-11 11:40:06 -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 47e96a027b Add color pallete for changing wire color 2026-05-11 09:35:46 +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
David Montero Crespo 176dbb79f3
Merge pull request #160 from davidmonterocrespo24/fix-micropython-multi-file-esp32 2026-05-09 21:15:20 -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
David Montero Crespo 6049bd1d81
Merge pull request #159 from davidmonterocrespo24/fix-sensor-control-panel-missing-i18n-import
fix(editor): add missing useTranslation import in SensorControlPanel
2026-05-09 19:21:49 -03:00
davidmonterocrespo24 7edb0a6499 fix(editor): add missing useTranslation import in SensorControlPanel
Block 9 added `const { t } = useTranslation()` at line 50 but forgot the
matching `import { useTranslation } from 'react-i18next'`. The component
then crashes the moment a user clicks a sensor on the canvas with
`Uncaught ReferenceError: useTranslation is not defined`, taking the
whole simulator render tree down.
2026-05-10 00:16:03 +02:00
David Montero Crespo 4a04c5c7c7
Merge pull request #158 from davidmonterocrespo24/fix-bmp280-test-metadata-shape
test(metadata): drill into components array for BMP280 lookup
2026-05-09 18:54:54 -03: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
David Montero Crespo 7c84d85a22
Merge pull request #157 from davidmonterocrespo24/compile-stdout-streaming
feat(compile): stream live ESP-IDF cmake + ninja output to the console
2026-05-09 18:37:44 -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 e1eec8124b
Merge pull request #156 from davidmonterocrespo24/actions-cache-cleanup
ci: cap Actions storage growth — buildx mode=min + auto-cleanup workflow
2026-05-09 18:27:41 -03:00
davidmonterocrespo24 5a00f1a380 ci: cap Actions storage growth — buildx mode=min + auto-cleanup workflow
Hit the 0.5 GB Actions storage quota today. Two-pronged fix.

1. docker-publish.yml: cache-to switched from mode=max to mode=min.
   With mode=max, buildx pushes every intermediate layer of the
   multi-stage build (qemu-provider, espidf-builder, frontend-builder,
   final stage) into the GHA cache. For our image that's easily
   500 MB-1 GB per cache update. mode=min stores only the layers used
   by the final image; incremental rebuilds still hit the cache for
   the meaningful steps but the footprint drops by roughly 60-70%.

2. actions-cache-cleanup.yml (new workflow):
   - Weekly schedule (Sun 04:00 UTC): deletes every cache older than
     14 days. Catches stale entries from deleted branches.
   - On `pull_request: closed`: deletes caches scoped to that PR's
     branch ref AND the merge ref. Buildx + actions/cache scope per
     branch, so a closed PR's caches are immediately stale — without
     this they linger until the GHA-default 7-day eviction.
   - Manual `workflow_dispatch` for one-shot runs when storage is
     already over.

Permissions: each job sets `actions: write` (the minimum needed for
cache deletion). No GH_TOKEN secret required; the default
GITHUB_TOKEN already has the scope.

Quota math after this lands:
  Before: every push to master = +500 MB-1 GB cache, kept 7 days
          → quota fills in 1-2 builds.
  After:  every push to master = +200-400 MB cache, plus old branches
          actively swept; 0.5 GB stays comfortable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:27:02 +02:00
David Montero Crespo a5aca2efb2
Merge pull request #155 from davidmonterocrespo24/standalone-volumes
fix(docker): VOLUME defaults so standalone runs get the perf caches too
2026-05-09 18:20:43 -03:00
davidmonterocrespo24 04eb36d0a2 fix(docker): VOLUME defaults so standalone runs get the perf caches too
A user reported on Discord that an ESP32 Blink compile on a fresh
`docker run -d ghcr.io/.../velxio:master` (no -v flags) takes 8-9
minutes — every single time. Same hardware that the local project
checkout flies through in 5-30 seconds with the new persistent build
dir + ccache pipeline.

Root cause: `docker run` without `-v` mounts gets nothing persistent.
ccache + persistent build dir live in /var/cache/ccache and
/var/lib/velxio-build, both wiped on every `docker rm` (which the
user explicitly did when troubleshooting). docker-compose users get
the volumes via the compose file; standalone users got nothing
because the Dockerfile didn't declare them.

This PR closes that gap.

Dockerfile.standalone
- VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino",
          "/var/cache/ccache", "/var/lib/velxio-build"]
  Anonymous volumes are now created automatically when the user runs
  the image without -v. They survive `docker stop`/`docker start`/
  `docker rm` (only `docker rm -v` or `docker volume prune` removes
  them). Users can still pass `-v` for named volumes — explicit
  mounts always win over the VOLUME directive.
- Replace `ccache --set-config max_size 8G` (RUN, written to
  /var/cache/ccache/ccache.conf which the volume mount masks at
  runtime) with `ENV CCACHE_MAXSIZE=8G` etc. — env vars override any
  conf-file value on every ccache invocation, so the 8 GB cap
  actually applies at runtime regardless of what's in the volume.

README.md
- Update both the quick-start docker run and the detailed self-host
  section to include all five volumes.
- Add a note explaining what each volume is for and that without them,
  cold compile times are 5-7 min vs the 5-30 s warm path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:19:34 +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