Skip to content

refactor(http): build the web UI with Vite and embed it gzipped in the firmware - #68

Draft
Mechazawa wants to merge 21 commits into
jamro:mainfrom
Mechazawa:task/embed-ui-in-firmware
Draft

Mechazawa wants to merge 21 commits into
jamro:mainfrom
Mechazawa:task/embed-ui-in-firmware

Conversation

@Mechazawa

Copy link
Copy Markdown
Contributor

What

Alternative to #60 with the same frontend work, but the page ships inside the firmware instead of on LittleFS. Pick one of the two.

  • The control panel moves out of the raw string in src/http/index_page.cpp into ui/ (ES modules, Vite, Vitest), exactly as in refactor(http): serve web UI from LittleFS, bundled with Vite #60.
  • Vite with vite-plugin-singlefile builds one self-contained, minified index.html. scripts/embed_ui.py (a PlatformIO pre-script) gzips it into a generated header, $BUILD_DIR/generated/web_ui.h, and the firmware serves those bytes with Content-Encoding: gzip straight from flash.
  • Because the UI is part of the firmware image, it can never be out of step with the API: no /ui/* routes, no version stamp or check, and -t ota / -t otafs / LittleFS work exactly as on main (audio only).
  • The page is 58 KB minified, 15.8 KB gzipped in the firmware, versus 77.7 KB uncompressed on main.
  • embed_ui.py skips Node when nothing under ui/ changed, so uploadfs, otafs and no-op builds do not rebuild the UI; an identical result does not recompile index_page.cpp.
  • Busy state now re-enables only the controls it disabled, and servo range changes notify the servo page and the calibration step, so callers no longer refresh them by hand.
  • Building the firmware needs Node 20.19+ on 20.x, or 22.12+, alongside PlatformIO. The firmware CI job and the release workflow install Node 22.

Checks

  • Title is type(scope): summary
  • Breaking HTTP / pins / NVS / servo defaults / hook CLI called out as type!: plus a BREAKING CHANGE: footer — none; routes and responses are unchanged
  • Firmware: pio run and/or pio test -e native when that code changed — pio run (default, ota, expression-demo), pio test -e native, python3 scripts/test_audio_pack.py, npm run lint --prefix ui, npm test --prefix ui
  • Packages: npm test --prefix packages/tiny-engineer-cursor / …/tiny-engineer-antigravity / …/tiny-engineer-claude-code when that package changed — N/A, no package changes
  • HTTP: HTML index (ui/index.html) + docs/api.md (and README / docs/hardware/testing.md if the route list changed) — route list unchanged; the HTML index now lives in ui/index.html
  • Settings: layers in docs/settings.md; no raw access_token in logs — no new settings; docs/settings.md web UI section points at ui/
  • CAD: .f3d + exported .3mf; CERN-OHL-S; AiEmblem.3mf not used as a branding swap — N/A
  • Pins: include/pins.h + docs/hardware/ together — N/A
  • Servo ranges not widened without a real robot — the setup wizard bar stays display-only; calibration moves only through the step buttons
  • Hardware tested: robot / bench / N/A — robot: firmware flashed over OTA, every page loaded in a headless browser with no JS errors, the served page decompresses byte-identical to the build. The setup wizard (setup AP mode) was not exercised on hardware.
  • No .env, tokens, or Wi-Fi passwords in logs or screenshots

PCB (if this PR changes a board)

N/A — no board changes.

Mechazawa and others added 21 commits October 6, 2026 09:21
The calibration bar was display-only, so joints could only be moved with
the nudge buttons. It is now a range input that moves the servo on
release and is disabled while a move is in flight.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The generic form-group range margin outranked the class selector and
pushed the slider below the drawn track.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The control panel now lives in ui/ as plain HTML, CSS and JS. pio run
gzips it into data/ui/, the firmware streams the page and /ui/* assets
from LittleFS with Content-Encoding: gzip, and a missing filesystem
image returns a 503 with upload instructions. Firmware shrinks by about
72 KB.

The markup is linted with html-validate, locally and in CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The control panel script is split into ES modules under ui/src/ and
written as modern JS. Vite bundles and minifies JS and CSS,
html-minifier-terser minifies the HTML, and pio run builds the UI before
gzipping it onto LittleFS. Hashed assets under /ui/assets/ are served
as immutable.

ESLint, Prettier and html-validate run via npm run lint in ui/. Building
the firmware now needs Node 20.19+.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Prefix matching sent /animations and /tests to the robot instead of the
Vite dev server.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Adds @stylistic/padding-line-between-statements and curly: all to the UI
ESLint config and applies them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Applies the standard config: modern color notation, unprefixed
appearance, kebab-case LED color classes (led-r/led-g/led-b), and
button state rules grouped at the end so specificity ascends through
the file without changing which rule wins.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Anchoring at the end of the path missed /settings?… and /anim?…, so
saves and animation requests stayed on the Vite dev server.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
/setup/servo skips the safe-range clamp, so a drag could command a large
jump and make parts collide or pull cables. The nudge buttons stay the
only way to move a joint during calibration; the bar shows the angle
and the saved band.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The build stamps the web UI with the firmware version, and the firmware
returns 503 naming both versions instead of serving a UI from another
build. -t ota now uploads the filesystem first, waits for the robot to
reboot, then uploads the firmware; -t otafs remains for recovery.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Each test loads the real index.html in jsdom, imports the UI modules and
drives them against a stubbed fetch that answers like the firmware and
records requests. Covers the auth gate, config saving and access token
states, the servo page range hint, and the setup wizard: step order,
what each step saves, step-only calibration moves and LED order
validation. CI runs npm test after the lint step.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The inline page in src/http/index_page.cpp lives in ui/index.html on
this branch, so the POST /play route, its name values and the talking
animation are added there. The dev server proxies /play to the robot.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Vite builds ui/ into one self-contained page (vite-plugin-singlefile),
and scripts/embed_ui.py gzips it into a generated header that the
firmware serves with Content-Encoding: gzip. The UI always matches the
firmware it ships in, so LittleFS only holds audio again: the /ui/*
routes, the version stamp and check, and the filesystem-first OTA order
are gone. The pre-script skips Node when ui/ is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VUaQRAXtYTdrXQ4C4us2V5
Busy state re-enables only the controls it disabled, so the wizard no
longer re-renders to undo it. Servo range changes notify the servo page
and the calibration step instead of every caller refreshing both, and
the servo_mins/servo_maxs format lives beside the ranges.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VUaQRAXtYTdrXQ4C4us2V5
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jamro

jamro commented Oct 8, 2026

Copy link
Copy Markdown
Owner

I like this direction... embedding the gzipped UI in the firmware feels like a cleaner fit for Tiny Engineer and removes the firmware/LittleFS version-coupling problem entirely.

Since #60 has now been squash-merged, could you rebase this branch onto the new main so the PR only shows the embedded-UI delta?

One small thing I noticed while reviewing the build script: embed_ui.py uses source mtimes to decide whether the generated header is still valid, but ui/package.json is not included in that list. It would be good to include it as an input as well so changes to the build configuration can’t accidentally reuse a stale web_ui.h.

Other than that, the approach looks solid to me.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants