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>
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>
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>
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>
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.
The /examples gallery is fully localised:
- Header (heading + subtitle).
- Search input placeholder + aria-label + the clear button.
- Match-count tag with i18next pluralisation (handles _one /
_other and Russian's _few / _many).
- Category and Difficulty filter labels + their button labels
(basics / sensors / displays / communication / games / robotics
/ circuits; beginner / intermediate / advanced).
- Per-card "Copy shareable link" tooltip.
- Empty-state copy with two variants (with-search / without-
search) interpolating the search query.
- Reset-filters button.
- The library-install progress overlay copy from
ExamplesPage.tsx ("Installing libraries (N/M)") with done/total
interpolation.
Internal /editor link uses localize() so a Spanish reader who
clicks an example lands on /es/editor.
Hand-translated for all 8 non-English locales. Per-example titles
+ descriptions are NOT i18n yet — they live in the
src/data/examples* tables and would need a separate pipeline.
DocsPage (2715 lines of prose) deferred too — best handled by
running scripts/translate-i18n.mjs once the keys are extracted.
The visible chrome of /about now reads from i18n in all 9 locales:
- Hero title + subtitle
- 7 section headings (Story / How It Works / Open Source Philosophy /
Creator / Recent releases / Community & Press / CTA)
- Final CTA card (title, subtitle, "Open Editor" button)
- Footer copy switched to t('footer.about') so it shows the AGPLv3
About-Velxio paragraph instead of the stale MIT/avr8js credit
- Footer + CTA Links wrapped in localize() so /es/about's "Open Editor"
routes to /es/editor
Long-form prose (Story body paragraphs, Open Source Philosophy
paragraphs, Creator bio, Releases blurbs, Personal-story quote, Press
section) deliberately stays in English in this commit. Each is a
multi-paragraph chunk that benefits from a curated translation pass
rather than an inline machine pass — slate it for a follow-up.
Tech-stack tags (Java, Python, React, Docker, etc.) and the creator's
name + role + GitHub/LinkedIn/Medium link captions stay untranslated:
all proper nouns / brand identifiers.
Both auth forms now go through t('auth.login.*') and
t('auth.register.*'):
- Title + subtitle, email / password / username labels with
username placeholder and password-min-length placeholder.
- Submit button toggles between idle and loading states.
- "or" divider, "Continue with Google" button, and the
switch-to-other-form footer link.
- Inline validation errors: reserved username, username regex,
password length, plus the generic catch-all from the API.
Internal /editor and /login / /register links wrapped in
localize() so a Spanish user who registers stays at /es/editor
after success.
AboutPage (519 lines) deferred to its own commit — too dense for
the same change.
Final block. The Support section ("Support the project" / GitHub
Sponsors / Donate via PayPal) now reads from t('landing.support.*'),
and the footer link labels use t('header.nav.*') so they pick up
the same nav translations the header already ships.
This closes the LandingPage rewrite — every visitor-facing string
on velxio.dev/ goes through i18n now. Hand-translated for all 9
locales. "GitHub Sponsors" stays untranslated (proper product name).
Phase 2 remaining work:
- Editor (toolbar, file explorer, simulator canvas, component
picker, library manager, save/share modals, error toasts).
- About / Examples / Docs / Profile pages.
- Login + Register forms.
The Features section ("Everything you need") and the 6 cards
underneath (Real-Time SPICE Analog, 5 Emulation Engines, Custom Chips,
100+ Components, Live Instruments, Monaco Editor + arduino-cli) now
read from t('landing.features.<key>.{title,desc}') for all 9 locales.
Refactor:
- The `features` array in LandingPage.tsx no longer carries title/desc
literals — only an icon + a translation key. The render maps each
card's key to the matching i18n entry. Cleaner and keeps the JSX
structurally stable across languages.
Translations:
- All 8 non-English locales hand-curated. Technical / brand names
(ngspice-WASM, AVR8, ATmega328P, RP2040, ESP32-C3, CH32V003, QEMU,
Cortex-M0+/A53, ILI9341, NeoPixel, Wokwi Custom Chips API,
WebAssembly, .hex/.uf2/.bin, VS Code, arduino-cli) preserved
unchanged in every locale — those are precise nouns where any
translation would degrade meaning.
Replaces the visible header copy of the supported-hardware section
with t('landing.boards.*') keys:
- label "Supported Hardware"
- titleLine1 / titleLine2 ("Every architecture." / "One tool.")
- subtitle (the "19 boards across 5 CPU architectures..." paragraph)
The five engine cards underneath (avr8js, rp2040js, QEMU lcgamboa,
QEMU Xtensa, QEMU ARM) and per-board specs (e.g. "ATmega328p · 32 KB",
"RP2040 + WiFi") deliberately stay in English — those are accurate
technical specs / product names that don't translate, and mixing
locales inside a spec line would hurt readability more than it
helps.
Hand-curated translations for all 8 non-English locales.
Hero strings now go through `t('landing.hero.*')`:
- titleLine1 / titleAccent (split for the gradient span)
- subtitle (one paragraph; "19 boards / 48+ parts" stays inside the
string so locales can phrase the count naturally)
- ctaPrimary / ctaGithub
- trustLine (the "no signup / runs in browser / free & open-source"
reassurance line — was previously emitting NBSP-wrapped middle
dots; the localised versions use plain spaces, which is fine
visually)
- imageAlt (a11y for the editor screenshot)
Internal /editor link now goes through localize() so a Spanish reader
clicking the primary CTA stays at /es/editor instead of dropping
back to English.
Translations are hand-curated for all 8 non-English locales (es,
pt-br, it, fr, zh-cn, de, ja, ru). Brand names (Velxio, Arduino,
ESP32, Raspberry Pi, GitHub, AGPLv3) preserved as-is. The script
arrow "→" is kept in every locale because it carries directional
meaning that translates naturally across languages.
Block 2 (Boards / supported hardware), Block 3 (features grid),
Block 4 (Support / footer copy) and Editor strings still pending.
This is Phase 1 of multi-language support: the visible chrome (header,
footer, language switcher) and routing are wired up for all 9 locales
(en, es, pt-br, it, fr, zh-cn, de, ja, ru) — same set the blog at
velxio.dev/blog/ already supports. The Editor and the long-form
landing-page copy are still English-only and will be translated in a
follow-up.
Infrastructure
- frontend/src/i18n/config.ts: locale registry + LOCALE_META (htmlLang,
native name, og locale, dir). Mirrors pro/blog/src/i18n/config.ts so
cookie sync stays consistent.
- frontend/src/i18n/cookie.ts: read/write the velxio_locale cookie at
Path=/; Max-Age=1y; SameSite=Lax (Secure on HTTPS). The blog reads
the same cookie via an inline script in its Layout.astro.
- frontend/src/i18n/path.ts: getLocaleFromPath / stripLocaleFromPath /
localizedPath / switchLocale / blogUrlFor — match the blog's helpers
one-to-one.
- frontend/src/i18n/index.ts: i18next bootstrap. English bundle is
inlined synchronously for first paint; non-default locales are
lazy-loaded via dynamic import on demand. Initial locale is decided
in priority order URL > cookie > navigator > en.
- frontend/src/i18n/LocaleSync.tsx: top-level wrapper inside <Router>.
On every URL change loads the matching locale bundle, calls
i18n.changeLanguage, writes the cookie, and mirrors the locale onto
<html lang> and dir.
- frontend/src/i18n/useLocalizedNavigate.ts: useCurrentLocale,
useLocalizedHref, useLocalizedNavigate hooks for components that
build internal links.
Routing
- App.tsx: route table extracted to a single ROUTES array, then
registered twice — once at the root (default English) and once
nested under each non-default locale (`/<locale>/...`). Explicit
per-locale parent routes (rather than a generic `:lang` param) so
React Router never accidentally swallows a real top-level path
like `/circuit-simulator` as a locale segment.
Header / Footer
- LanguageSwitcher.tsx + .css: dropdown matching the blog's
LanguageSwitcher.astro. Globe icon + locale code on the trigger,
native names + ISO codes in the menu. Click → `switchLocale()`
rewrites the URL under the new locale; LocaleSync handles the
rest (load bundle, change language, write cookie).
- AppHeader.tsx: every nav label and the auth dropdown copy now
goes through `t('header.nav.*')`, `t('header.auth.*')`. All
internal Links wrapped with localize() so navigation stays
inside the active locale. Added a "Blog" link computed via
`blogUrlFor(currentLocale)` so /es/ → /blog/es/, etc.
- LandingPage.tsx: footer About-Velxio paragraph reads from
t('footer.about').
Translations (Phase 1 strings)
- frontend/src/i18n/locales/<locale>/common.json: nav labels, auth
buttons, footer About copy. Hand-translated for all 9 locales,
AGPLv3 / brand names preserved as-is.
Tooling
- frontend/scripts/translate-i18n.mjs: standalone Node script that
takes the en.json bundles and auto-translates them to the 8 other
locales via DeepSeek (primary) + Gemini (fallback). One LLM call
per (locale, namespace) pair. Run after extracting new strings
with `npm run translate:i18n`.
Phase 2 (deferred)
- Editor (toolbar, file explorer, simulator canvas, component picker,
library manager, error toasts) — hundreds of strings.
- Examples / Docs / About / Profile pages.
- The translate-i18n.mjs script is ready to handle these once the
strings have been extracted into JSON keys.
The previous footer credit ("MIT License · Powered by avr8js &
wokwi-elements") was wrong twice over: velxio is AGPLv3 (with a
commercial license available), and the project now ships much more
than the two libraries it singled out (rp2040js, eecircuit-engine,
QEMU, ESP-IDF, arduino-cli, Monaco editor, ...).
Replace it with a one-paragraph About Velxio that sits at the bottom
of the landing page, mirrors what the blog footer shows at
velxio.dev/blog/, and correctly states the AGPLv3 license.
Widen .footer-copy to max-width: 680px so the longer copy has room
to breathe and breaks across two lines on desktop.
UI-facing half of the undo/redo feature. Combined with the previous two
commits, Ctrl+Z (or the toolbar button) now reverses every canvas
mutation: add/remove component, move, rotate, set property, add/remove
wire.
EditorPage.tsx:
- New window-keydown effect for Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z (and the
Cmd equivalents). Uses the same input/textarea/contenteditable guard
as the existing Ctrl+S handler — Monaco's per-file undo and the AI
chat composer keep their own behaviour.
SimulatorCanvas.tsx:
- Two icon buttons (undo + redo) added to the canvas header, between
the board selector and the Serial Monitor toggle. Tooltip surfaces
the next command's description ("Undo: Add LED (Ctrl+Z)") so the
user knows exactly what's about to revert. Buttons disable when the
stack is empty in that direction.
- New `canvas-icon-btn` CSS class for square 32×32 icon-only buttons
(matches the visual weight of the existing Serial button without
the label).
- Subscribes to history / historyIndex via store selectors so the
buttons re-render reactively as commands are pushed/undone.
No new tests — the store-level coverage from 99ed22b already exercises
undo/redo round trips. UI affordances are wired pass-through to those
store APIs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace the "DMC" initials placeholder with the GitHub avatar
(https://avatars.githubusercontent.com/u/47928504?v=4). The CSS for
.about-creator-avatar already had the rounded frame; just swapped to
object-fit: cover so the <img> fills the circle correctly, plus a
subtle ring + drop shadow.
- Add a "Recent releases" section between the Creator block and the
personal-story quote, with two cards:
- Velxio 2.5 (Latest) → /v2-5 (ngspice-WASM analog co-simulation)
- Velxio 2.0 → /v2
Each card has a tagline + 2-3 line blurb. The 2.5 card gets a blue
border + "Latest" tag so it reads as the current launch. About now
surfaces both release pages, which previously were only linked from
the Circuit/Electronics/SPICE simulator pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes that ship to OSS — all benign for self-hosters, but most are
extension points the velxio-prod overlay (and any private fork) needs to
plug an in-editor AI chat into the page.
Editor:
- 3-way view-mode toggle (code / both / circuit) in the unified toolbar.
Lets users hide a pane to give a right-docked sidebar (e.g. the AI
chat overlay) more breathing room. Persisted in useEditorStore.
- Default file explorer narrower (210 → 165 px); min 110.
- Removed the redundant `tb-board-pill` (icon + "Editing: X" tooltip);
the BoardSelector dropdown elsewhere already shows the active board.
- Inlined Import/Export/Upload-firmware buttons; the 3-dot overflow
menu gave up too much discoverability. Removed dead overflow state.
Simulator:
- Fix: global Delete/Backspace handler in SimulatorCanvas no longer
fires when the event target is an INPUT/TEXTAREA/SELECT/contentEditable
— affected any in-page text field, not just the chat overlay.
Overlay extensibility:
- New `data-velxio-slot="agent-chat"` at the bottom of EditorPage so
pro overlays can portal a chat panel into the editor without
forking the page.
- vite.config.ts: preserveSymlinks=true when VITE_PRO_BUILD is set.
Lets local-dev junctions (overlay tree → frontend/src/pro) resolve
bare imports back to the OSS node_modules without resolving symlinks.
Deps:
- Added react-markdown + remark-gfm (rendered chat output) and
@google/genai + zod (overlay agent loop). Tree-shaken from the OSS
bundle when no pro code imports them.
gitignore:
- Ignore backend/app/pro/ and frontend/src/pro/ junctions used by
developers running a private overlay against the OSS dev server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the manually-positioned wokwi-element mock (CircuitPreview) in
the /examples gallery with real screenshots of the actual simulator
canvas, so each card shows the example's boards + components + wires
exactly as they appear when you open the example.
How it works
- A new <ExampleThumbnail> component tries
/examples-thumbs/<id>.{webp,png} first. If the image 404s — or no
thumbnail has been captured yet — it falls back to the existing
CircuitPreview component. No-op for examples without a screenshot.
- ExamplesGallery and ExampleDetailPage now render <ExampleThumbnail>
instead of CircuitPreview directly.
- Explicit example.thumbnail field still wins (kept the existing
override path in case someone wants a custom asset).
Capture pipeline
- Generated by velxio-prod's scripts/capture-example-thumbs.mjs
(Playwright + sharp). For each example: opens /examples/<slug>,
clicks "Open in Simulator", waits for [data-board-id] elements,
computes the bbox of all boards + components, sets a transform on
.canvas-world to center and fit them with 12% padding inside the
canvas viewport, screenshots .canvas-content, and re-encodes to
600x360 @2x DPI as .png + .webp.
This commit ships the first batch (102 of ~129 examples — the rest
will follow once the capture completes; missing slugs gracefully
fall back to CircuitPreview in the meantime).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The marketing copy and docs claimed ESP32-C3 / XIAO-C3 / SuperMini /
CH32V003 ran on a "browser-native RV32IMC core written in TypeScript",
but production runs through QEMU lcgamboa (libqemu-riscv32) with the
esp32c3-picsimlab machine — same backend pattern as Xtensa ESP32, just
a different libqemu binary. The TypeScript ISA layer
(RiscVCore.ts / Esp32C3Simulator.ts / RiscVSimulator.ts) is kept only
as Vitest unit-test infrastructure for RV32IMC instruction decoding;
it cannot handle the 150+ ROM functions ESP-IDF needs at boot and is
not wired into the production emulation path.
Files updated:
Marketing pages
- LandingPage: board-group label, FAQ answer, architecture description
no longer claim "browser-native" or "no backend needed" for RISC-V.
- AboutPage: arch card retitled "RISC-V via QEMU", body explains the
libqemu-riscv32 / lcgamboa backend.
- Velxio2Page: arch group engine label, multi-board feature item,
competitive-comparison card all corrected.
- ArduinoEmulatorPage: two RISC-V cards corrected.
- ESP32SimulatorPage: ESP32-C3 cross-link card corrected.
- ESP32C3SimulatorPage: hero subtitle, trust strip, supported-boards
intro, JSON-LD description corrected.
- ElectronicsSimulatorPage: install-needed FAQ corrected.
- examples.ts: c3-blink description and code-comment corrected.
SEO surfaces
- index.html: JSON-LD SoftwareApplication description, OS-fallback FAQ
body, supported-boards <ul> bullets, feature list bullets corrected.
- seoRoutes.ts: /esp32-c3-simulator title + description corrected;
homepage description corrected.
Docs page
- DocsPage RiscVEmulationSection: intro paragraph rewritten — RISC-V
goes through QEMU lcgamboa with libqemu-riscv32 / esp32c3-picsimlab,
TypeScript layer is Vitest-only.
- DocsPage Esp32EmulationSection callout: section now applies to all
ESP32 family (Xtensa + RISC-V), pointer to RISC-V doc clarified.
README
- "Boards" table: production-engine column for ESP32-C3 family and
CH32V003 changed from "RiscVCore.ts (browser)" to "QEMU lcgamboa
(backend)".
- "ESP32-C3 / XIAO-C3 / SuperMini / CH32V003" subsection retitled
"(RISC-V via QEMU)" — body explains libqemu-riscv32 backend and
flags the TypeScript layer as Vitest-only.
The two remaining "browser-native" hits in the codebase
(Velxio25Page:176, index.html:348) are about ngspice-WASM SPICE
analog simulation, which genuinely is browser-native — left alone.
Build verified: npm run build:docker succeeds, 246 SEO pages prerender.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures /examples/traffic-light → /editor in headless Chromium and saves
the rendered editor as a 3840x2160 (2x DPI) PNG + WebP for the landing
page hero.
The shot includes the code editor on the left (Traffic Light Simulator
.ino), the Arduino Uno on the canvas with three LEDs wired up, the SPICE
nets indicator, and the full chrome — a much stronger first impression
than the previous CSS-mocked schematic.
Generation script lives in the private velxio-prod repo
(scripts/capture-hero.mjs) and can be re-run any time to refresh the
asset against the live deployment.
- /marketing/hero-editor.png (320 KB)
- /marketing/hero-editor.webp (160 KB)
- LandingPage hero <picture> now points at these (loading=eager,
fetchPriority=high since it's above the fold).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation
- Add 7 token CSS files in src/tokens/ — semantic colors, 4-pt spacing,
Apple-HIG type ramp, radius/elevation/motion/z-index scales.
- Refactor src/index.css to import tokens and remap legacy aliases
(--accent, --bg, etc.) onto the new --color-* semantics so existing
components keep rendering during migration.
- Drop the duplicate font-family from src/App.css; body inherits from :root.
- Global *:focus-visible ring backed by --color-focus-ring (WCAG 2.4.7).
Webfonts (self-hosted)
- Add Inter.var.woff2 (variable, OFL) and JetBrainsMono.var.woff2 to
public/fonts/. Preloaded in index.html with crossorigin.
- Old stack -apple-system kept as fallback so Mac users still get SF Pro.
- Fixes cross-OS rendering inconsistency (Win/Linux/Android were falling
back to Segoe UI / Roboto, breaking the type grid).
Component primitives
- New src/components/ui/{Button,Card,Input}.tsx + .css. Built on the
semantic tokens, ready for incremental migration of .ap-* CSS classes.
Lucide icons
- Replace 6 inline SVG icon components in LandingPage (IcoChip / IcoCpu /
IcoCode / IcoZap / IcoLayers / IcoMonitor) with lucide-react imports.
Aliased so call sites are unchanged. ~80 lines of inline SVG removed.
- IcoGitHub kept bespoke (filled glyph, brand-correct).
Marketing assets
- Convert top 8 boards to transparent PNG + WebP at 1x / 2x:
Arduino Uno, Nano, Mega 2560, Pi Pico, Pi Pico W, ESP32-C3,
ESP32-DevKit-V1, XIAO ESP32-S3.
- Migrate matching cards in LandingPage and Velxio2Page to <picture>
with WebP > PNG > SVG fallback. Other 8 boards keep <img src=*.svg>
for now (Raspberry Pi 3B, ESP32-CAM, etc.).
- Refresh og-image.png — same canonical URL, new content (4 hero boards
+ branding instead of generic logo card).
- Fix latent bug in LandingPage: ESP32 DevKit V1 card was loading
esp32-devkit-c-v4.svg; now uses esp32-devkit-v1.{webp,png,svg}.
Build verified: npm run build:docker succeeds, 246 SEO pages prerender.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous hero ("Circuits + Code. / One Browser Tab. / SPICE-accurate.")
was optimised for EE engineers searching for circuit simulators. Most
visitors land here looking for an Arduino emulator they can use without
installing anything — the names of the supported boards are a stronger
hook than analog-simulation accuracy.
Restored the older "Arduino, ESP32 & Raspberry Pi. / Right in your
browser." framing and tightened the subtitle to action verbs (Write,
wire, run) plus concrete numbers (19 boards, 48+ parts). Drops:
- "SPICE-accurate" — kept on the dedicated /arduino-emulator,
/circuit-simulator etc. SEO landing pages where the audience is
actively looking for it
- "ngspice", "co-simulated", "custom chips in C or Rust" — niche, fit
better in the features section below
Two upstream additions to support private overlays implementing paid tiers
without forking client code:
- store/useAuthStore.ts: UserResponse extended with optional
is_paid_subscriber, subscription_status, subscription_period_end. The
backend now returns these in /api/auth/me; the persist middleware
serialises them automatically.
- pages/PricingPlaceholder.tsx (NEW): the /pricing route. Renders a polite
"this image is fully free" message for self-hosters plus a
data-velxio-slot="pricing-page" target where private overlays can
portal-inject a real pricing page.
- App.tsx: register the /pricing route after /about.
Self-hosted OSS image: /pricing shows the placeholder, no behavioural
change anywhere else. Production with a private overlay: /pricing shows
the overlay's full pricing UI.
Frontend build verified.
Three small markers (each one HTML attribute) so private overlays can
portal-inject UI into well-defined places without forking the upstream
component:
- AppHeader user dropdown: data-velxio-slot="user-menu"
Lets overlays add menu items between "My projects" and "Sign out"
(e.g. a Privacy / opt-out item).
- AdminPage tab bar: data-velxio-slot="admin-tabs"
Lets overlays add extra tabs alongside Dashboard / Users / Projects /
Boards (e.g. a Pro Analytics tab).
- AdminPage tab content area: data-velxio-slot="admin-tab-content"
Sibling div where overlay tab content can portal-render.
Generic markers, no overlay-specific code in upstream. Anyone with
private extensions can use them. The OSS build is otherwise unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves several install pain points reported by users (#108, #120) and
removes the obligatory upstream-clone step that confused contributors and
slowed down every Docker build.
Install fixes:
- nginx: server_name → catch-all default_server, drop Debian's stock site
so reverse-proxied users no longer get the "Welcome to nginx" page.
- entrypoint: auto-generate SECRET_KEY at first boot, persisted under
data/.secret_key. backend/.env is now optional in docker-compose.yml.
- backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on
some Python builds — caused uvicorn startup failures on WSL).
Wokwi libs come from npm:
- @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in
frontend/package.json. Vite aliases removed.
- Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements
/ wokwi-boards. Frontend stage is just COPY + npm install + build:docker.
- Board SVGs vendored under frontend/public/boards/ (10 deduped against
existing files, 2 truly new). third-party/wokwi-* clones become reference-
only credits — generate-component-metadata.ts skips gracefully when absent.
Production config split out:
- docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf,
update-third-party.bat removed. Production deployment lives in its own
repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups
+ pinned upstream commit).
Verified locally: 1161 frontend tests pass, build:docker completes clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.
Mechanical changes:
Path rename:
wokwi-libs/ → third-party/
update-wokwi-libs.bat → update-third-party.bat
docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md
Submodule reconfiguration:
.gitmodules — 4 path= and section names updated
.git/modules/wokwi-libs/ → .git/modules/third-party/
each submodule's .git file rewired to ../../.git/modules/third-party/<name>
Reference updates (~80 files): vite.config.ts aliases, Dockerfile
COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
docs/* and test/*/autosearch/* entries that mention the path,
package-lock.json file: dependencies, .gitignore patterns,
sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
.dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
(path) and bare `wokwi-libs` (textual mentions in docs/comments).
Verified:
- npx tsc -b --noEmit produces no new errors related to these paths
- vite.config.ts aliases now point at ../third-party/avr8js etc.
- All 4 git submodules (avr8js, rp2040js, wokwi-elements,
wokwi-features) are linked under third-party/ with their
worktrees re-populated and config files referencing the new path
- `grep -r wokwi-libs` returns zero hits outside node_modules,
.vite, frontend/dist, third-party/ (upstream submodule contents),
*.pyc caches, and *.dll.pre-camera rollback binaries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The project save/load pipeline only persisted a single `board_type`, so
multi-board workspaces silently lost every board except the active one
on save, and wires referencing the dropped boards' IDs orphaned to the
canvas corner on reload. An audit of the production backup found 74/306
projects (24%) with at least one orphaned wire and 174/301 non-trivial
projects whose code was still the default Blink template — strong signal
that users save once and never re-save.
Backend
- Add `boards_json` column on `projects` with idempotent ALTER TABLE in
the lifespan migration list.
- New `FileGroup` schema + `file_groups` array on
ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat.
- `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via
`read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on
read; legacy single-list `files` only updates the active group, leaving
other boards' files intact.
- `_persist_files_from_body` honors file_groups → files → code priority.
Frontend
- `useSimulatorStore.addBoard` accepts an optional `explicitId` so
saved board IDs can be restored verbatim (wires reference IDs literally).
- New `loadProjectState({boards, fileGroups, components, wires,
activeBoardId})` action: tears down current boards, recreates from the
payload, restores file groups atomically, recalculates wire positions
on the next frame, and refreshes the Interconnect.
- `useEditorStore.replaceFileGroups` for atomic multi-group restore.
- `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through
`buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects
by synthesising a default board from `board_type`).
Auto-save (#useAutoSaveProject hook)
- 2.5s debounced silent PUT triggered ONLY when an authenticated user
has a `currentProject` with a UUID. State hash detects real changes
vs. UI-only churn; baseline is reset on project load so the just-loaded
state isn't immediately re-saved.
- `beforeunload` flush via `fetch keepalive: true` (supports PUT +
credentials, survives unload).
- Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error).
Backfill script (one-off, idempotent)
- `backend/scripts/backfill_boards_2026_05.py` populates `boards_json`
for legacy projects. Heuristic per project, based on which board IDs
the wires reference:
Case A — wires only ref 'arduino-uno' but board_type ≠ uno:
rename id→board_type and rewrite wire endpoints.
Case B — single-board normal: keep verbatim.
Case C — multi-board: recreate one board per distinct ref, infer
kind by stripping trailing -N suffix.
Also moves any flat files into the active board's group subdir.
Stdlib-only, runs from host or `docker exec`.
Docker
- `Dockerfile.standalone` now copies `backend/scripts/` into the image
so the backfill is callable via `docker exec velxio-app python
/app/scripts/backfill_boards_2026_05.py --apply`.
Verified locally on the restored production backup (363 projects):
33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans.
Re-running the script after apply skips all 363 (idempotent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Implemented handshake tests to validate initial bus state and register responses.
- Created end-to-end tests for Pico W LED blinking using MicroPython firmware.
- Added SDPCM framing tests to ensure proper encoding and decoding of control frames.
- Developed IOCTL tests to verify command responses and state changes in the emulator.
- Established a full lifecycle test for WiFi operations, including scanning, connecting, and packet handling.
- Introduced TypeScript configuration for test files to ensure compatibility and strict type checking.
Track per-user, per-project, and per-board usage to inform pricing tier
decisions. Adds an admin dashboard with KPIs (DAU/WAU/MAU, totals,
success rate), time-series charts for compiles/runs, board family +
FQBN breakdowns, "board diversity" pie chart (key freemium signal),
top users/projects, and per-country breakdown via Cloudflare's
CF-IPCountry header. Admin can now also view private projects.
Backend:
- New UsageEvent table (append-only event log with user_id, project_id,
event_type, board_fqbn/family, country, error_kind, duration_ms)
- Aggregate counters on User (total_compiles/runs/errors, last_active,
signup_country, last_country) and Project (compile/run/update counts,
last_compiled/run timestamps) kept in sync by MetricsService for O(1)
dashboard reads
- 10 admin endpoints under /api/admin/metrics/{overview, timeseries,
boards, board-diversity, top-users, top-projects, countries,
users/{id}, projects/{id}}
- POST /api/metrics/run for client-side run telemetry
- Country detection via cf-ipcountry header (no DB / no API calls)
- Auto-migrations in lifespan for legacy DBs
Frontend:
- recharts-powered Dashboard tab with KPI cards and 4 charts
- New Boards tab with per-family + per-FQBN breakdown
- Country column with flag emoji on Users tab
- Top countries card on Dashboard
- compileCode now forwards project_id; Run button reports via WS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the /v2 page structure but targets the 2.5 launch: ngspice-WASM
analog simulation, hybrid digital + analog co-simulation with Arduino /
ESP32 / RP2040, expanded component catalog, live instruments, 40 new
analog/hybrid examples.
- Reuses Velxio2Page.css + SEOPage.css — no new stylesheet to maintain
- Adds SoftwareApplication, BreadcrumbList, and FAQPage JSON-LD for
rich-results eligibility
- Registers the route in App, entry-server (SSR prerender), and
seoRoutes (sitemap, priority 0.95 / changefreq weekly)
- Modified the index file to reflect the new naming convention for Velxio components.
- Changed JSX declarations to use 'velxio-' prefix for various components.
- Updated component overrides to replace 'wokwi-' with 'velxio-' for logic gates and other components.
- Adjusted SVG generation script to use 'velxio-' prefix for BMP280 and Raspberry Pi components.
- Marked submodules as dirty in QEMU and RP2040 libraries.
- Added .prettierignore and .prettierrc.json for consistent code formatting.
- Introduced InstrumentComponent with support for Voltmeter and Ammeter, including pin information handling.
- Implemented Bmp280Element as a custom web component for the BMP280 barometric sensor, including SVG representation and pin configuration.
- Created CircuitPreview component to render circuit thumbnails using SVGs of components, including support for various boards and components.
- Added a script to generate SVG files from wokwi-elements, ensuring proper formatting and structure for reliable rendering.
- Introduced a test HTML generation script to visualize component SVGs.
- Implemented ExampleDetailPage for individual example projects with SEO metadata.
- Updated routing to use ExampleDetailPage instead of ExampleLoaderPage.
- Enhanced sitemap generation to include example project URLs.
- Added prerendering support for example detail pages in the server entry.
- Improved SEO handling in ProjectByIdPage to dynamically set metadata based on project visibility.
- Refactored example ID extraction from examples.ts for sitemap generation.
- Updated console logs to reflect total URLs generated in sitemap.
Two bugs causing "can't save project" reports:
1. SaveProjectModal: validate currentProject.id is a real UUID before
calling updateProject. If id is "none" or any non-UUID string, fall
through to createProject instead, avoiding PUT /api/projects/none.
2. ProjectByIdPage: call clearCurrentProject() when the project fetch
fails (404/403/error). Prevents stale project IDs from a previous
session polluting the store and triggering spurious update calls.
- Implemented ExampleLoaderPage to load examples by ID from the URL.
- Added ExampleLoaderPage route to App component.
- Created ShareModal for sharing project links with visibility toggle.
- Updated UserProfilePage to include share button for user projects.
- Enhanced ExamplesGallery with a copy link button for examples.
- Introduced utility function loadExample to streamline example loading and library installation.
- Updated project visibility management in useProjectStore.
- Added styles for new components and buttons.
- Updated .gitignore to include Arduino compilation byproducts.
- Resize image.png from 2816x1536 to 1400x764 (7.3MB → 1.9MB, -74%)
- Add image.webp version (201KB, -97% vs original)
- Update nginx.conf to auto-serve WebP via Accept header negotiation
- Update LandingPage.tsx to use <picture> with WebP source + lazy loading
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>