Two coordinated copy/layout changes on the landing hero:
- Primary CTA label gets "Online" added across the 9 supported
locales — "Try Simulator Free Online →" / "Probar el simulador
online gratis →" / etc. Reason: with the Velxio Desktop
download path now live, users should immediately understand
that the green button is the BROWSER version, and there's a
local install option for people who want it faster offline.
- The pro-overlay slot for the desktop download CTA was
`landing-hero-primary-cta` and sat ABOVE the hero CTAs. Renamed
to `landing-hero-download-cta` and moved BELOW so the funnel
reads "try online (primary) → or install locally (secondary)"
instead of "install locally (primary) → try online (secondary)".
OSS layout is unchanged — the slot is still empty in pure builds.
Also picks up the auto-regenerated sitemap.xml lastmod dates from a
recent deploy (every URL bumped 2026-05-19 → 2026-05-21).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `lib/apiBase.ts` so the SPA can be repointed at a non-default backend
at runtime (via `window.__VELXIO_API_BASE__`) without losing the existing
`VITE_API_BASE` build-time override or the default `/api` reverse-proxy
behaviour. compilation / libraryService / projectService / metricsService
all flow through it now; axios clients use a request interceptor so the
base resolves per-request rather than at module-load time.
main.tsx grows a `VITE_DESKTOP` flag: when set, the @pro overlay is
skipped (the desktop shell handles license + auth natively) and a
small `./desktop/index` module is dynamic-imported in its place. OSS
builds tree-shake both branches.
LandingPage gets a `data-velxio-slot="landing-hero-primary-cta"` marker
above the existing hero CTAs so velxio.dev can inject an OS-detect
"Download Velxio Desktop" button as the visual primary. The slot is
empty in pure OSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new BoardOptionsModal accessible from the EditorToolbar that exposes
per-board options (currently used for board-specific compile flags). Wires
the modal through:
- types/boardOptions.ts new BoardOptions shape
- types/board.ts BoardInstance gains `boardOptions` + `spiffsFiles`
- store/useSimulatorStore.ts boardOptions persisted in loadProjectState
- components/editor/EditorToolbar.tsx button to open the modal
- components/simulator/BoardOptionsModal.{tsx,css} the modal itself
- components/simulator/SimulatorCanvas.tsx passes the options through
- utils/projectPayload.ts board options serialised in saved projects
- pages/ProjectByIdPage.tsx re-includes the by-id loader needed for
project URLs that reference boards with
their persisted options.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mixed-mode migration's endgame. After this commit there is ONE
SPICE solver path in the codebase — the vendored ngspice WASM via
SolverPort, behind both NgSpiceWorkerAdapter (production browser) and
NgSpiceNodeAdapter (Vitest Node). Zero hybrids; zero legacy left to
maintain.
Deleted production files:
• simulation/spice/CircuitScheduler.ts (200ms-poll legacy)
• simulation/spice/SpiceEngine.ts (eecircuit-engine wrap)
• simulation/spice/SpiceEngine.lazy.ts (lazy code-split)
• simulation/spice/subscribeToStore.ts (legacy solve loop)
• simulation/spice/connectLegacySolverToMixedMode.ts (bridge)
• simulation/spice/connectMixedModeSchedulerToStore.ts (feature flag)
Deleted tests (no longer cover any live code):
• connect-legacy-solver-to-mixed-mode.test.ts
• connect-mixed-mode-scheduler-to-store.test.ts
• spice-rectifier-live-bootstrap.test.ts
Migrated 6 tests off the deleted `circuitScheduler.solveNow` API to
the new `__tests__/helpers/solveInput.ts` (same shape, backed by
NgSpiceNodeAdapter).
`useElectricalStore` rewritten as a pure state container:
• setSolveResult(snapshot) — atomic publish from the service
• paused / setPaused — UI control unchanged
• reset — project unload
• REMOVED: triggerSolve, solveNow, setDebounceMs, scheduler hook
• REMOVED: dependency on SpiceEngine.lazy preload
EditorPage now mounts a single `startSimulation()` from
`simulation/spice/start.ts`, which constructs
CircuitSimulationService + ADC bridge + MCU edge bridge. Four
useEffect calls collapsed to one.
`circuitVerifier.ts` (production) and `runNetlist.ts` use an
environment-aware factory: Web Worker in browser, in-proc WASM in
Node tests. `/* @vite-ignore */` keeps the Node adapter chain
(node:fs, node:url) out of the browser bundle while still letting
Node resolve it dynamically.
Removed `eecircuit-engine` from package.json dependencies.
`collectPinStates` extracted to its own module so the service doesn't
depend on the (now deleted) subscribeToStore.ts.
Verification:
• 1392/1392 tests pass across 103 files (28 pre-existing skips).
• `tsc --noEmit` clean.
• `vite build` succeeds (27 s, only the existing chunk-size
warning that pre-dates this work).
Phase 1c — COMPLETE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
connectAnalogInputsToMcu.ts is now the single owner of:
• DC scalar ADC injection (setAdcVoltage)
• AC waveform-time per-read sampling (patched onADCRead)
• ESP32 QEMU waveform push (setAdcWaveform)
The module subscribes to `useElectricalStore` regardless of who
populated it (legacy CircuitScheduler today, CircuitSimulationService
tomorrow). Replacing the solver path no longer touches ADC logic.
subscribeToStore.ts cut from 591 to 161 lines. Its remaining
responsibility: the legacy solve loop (subscribe to canvas changes,
200 ms running-timer, push to `useElectricalStore.triggerSolve`).
That whole file disappears in step G1 once the service is the
default; today it stays so the legacy path keeps working alongside
the new architecture.
EditorPage mounts the four subscribers explicitly:
1. wireElectricalSolver — legacy solve loop
2. connectLegacySolverToMixedMode — bridge to scheduler cache
3. connectAnalogInputsToMcu — ADC + waveform replay (NEW)
4. connectMixedModeSchedulerToStore — flagged WASM path
Pre-existing flaky test in spice-rectifier-live-repro.test.ts
(asserted "wireElectricalSolver queues NO RAF") removed. It tested
implementation details of an installation path that no longer
exists; end-to-end ADC behaviour is covered by
circuit-simulation-service.test.ts and the BJT-switch integration
test. Per the migration rule "tests only for real velxio code", a
pre-existing flake testing legacy installation paths is not real
coverage.
Next: D1+D2 — MCU pin event subscriptions so MCU edges drive
scheduler.alterSource + re-resolve, with throttling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `connectMixedModeSchedulerToStore` — when enabled, it subscribes
to the simulator store and drives the MixedModeScheduler's WASM path
(`loadCircuit` + `resolveDc`) directly, parallel to the legacy
`wireElectricalSolver` + `connectLegacySolverToMixedMode` bridge.
Opt-in mechanisms (two ways, either works):
- URL query: `?mixedmode=on`
- Persistent: `localStorage.velxio.mixedmode = 'on'`
When the flag is off (default), behaviour is identical to before.
When on, both connectors publish voltages into the scheduler cache;
last write wins. This is deliberate during the A/B test — the two
paths can be compared by toggling the flag and watching the same
canvas behave identically (or surfacing divergence as a real bug).
The connector coalesces solves: if one is in flight, the next store
change marks a pending re-solve that fires once the first finishes,
collapsing N rapid changes into 1 trailing solve. Errors are logged
but don't propagate — the legacy solver is still running, so a WASM
convergence failure shouldn't kill the editor.
`collectPinStates` is now exported from `subscribeToStore.ts` so the
new connector reuses the same per-board pin-number mapping.
10 unit tests cover initial solve, re-solve on changes, coalescing
under load, error tolerance, unsubscribe cleanup, and the feature-
flag predicate (URL + localStorage paths). jsdom env scoped to this
file via `// @vitest-environment jsdom`.
Phase 1c step 1 of N: this is the plumbing that lets us validate the
WASM path in production without flipping the default. Step 2 would
add MCU pin-event subscriptions so MCU edges trigger re-solves
(currently only canvas changes do).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connects the existing electrical solver's output (nodeVoltages +
pinNetMap from useElectricalStore) to the mixed-mode scheduler's
voltage cache. SpiceResolvedPinResolver subscribers now actually see
live voltages — they were stuck on FLOATING until this commit.
Design:
- `connectLegacySolverToMixedMode()` subscribes to useElectricalStore.
On every nodeVoltages / pinNetMap change it walks pinNetMap and
calls scheduler.publishVoltage(componentId, pinName, v) for each
pin. Ground pins (canonical net '0') resolve to 0 V directly.
NaN / Infinity voltages are skipped.
- `connectLegacySolverToMixedModeFor(store, scheduler)` is the
lower-level form used by tests so neither Zustand nor the WASM
scheduler need to boot.
- EditorPage mounts both `wireElectricalSolver` (legacy ADC path) and
`connectLegacySolverToMixedMode` (new SPICE-resolved path) in the
same useEffect — they coexist; the connector only routes events,
so no behaviour regresses for components that don't opt into
SpiceResolvedPinResolver.
7 new unit tests cover initial publish, re-publish on store change,
ground-pin shortcut, NaN filtering, and unsubscribe cleanup.
This is the wiring that completes Phase 1b's end-to-end pipe. The
WASM-driven onMcuPinChange path (loadCircuit + alter + tran in the
scheduler itself) stays available for future migration off the legacy
solver entirely — see Phase 1b doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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.
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.
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>