Semver firmware versioning + update-available badge (stable + dev channels)#27
Semver firmware versioning + update-available badge (stable + dev channels)#27ewowi wants to merge 4 commits into
Conversation
The Firmware card's version is now pure semver (2.1.0-dev on dev builds, 2.0.0 on a stable release) instead of the redundant "2.0.0 (v2.0.0)" — the release channel is derivable from the prerelease suffix, so it's no longer mixed in. On top of that, a status-bar badge appears when a newer stable GitHub release exists for the device, and clicking it opens the Firmware card with that release pre-selected, one click from installing. KPI: 16384lights | PC:383KB | tick:115/87/116/9/1/313/36/16/19/115/11us(FPS:8695/11494/8620/111111/1000000/3194/27777/62500/52631/8695/90909) | ESP32:1227KB | src:97(20045) | test:69(10600) | lizard:75w Core: - FirmwareUpdateModule: version control is now pure semver (kVersion) — dropped the (kRelease) channel concatenation; the channel is derivable from the prerelease suffix. - HttpServerModule: serve the new /semver.js asset. Light domain: - (none) UI: - semver.js (new): dependency-free Semantic Versioning parse + precedence-correct compare + isNewer, per semver.org §11. One home for version comparison; our own code, no npm dep. - app.js: status-bar firmware-update badge — compares the device version to GitHub's newest stable release (releases/latest, prereleases excluded), shows only when newer AND a compatible .bin exists. Cached in localStorage (1 h TTL) so it doesn't slow page load, with a forced fresh check when the Firmware card opens. Click pre-selects the release in the picker and opens the Firmware card. Best-effort: any failure hides the badge. - index.html / style.css: the badge element + accent-pill style in the status bar. - embed_ui.cmake: embed + serve semver.js alongside the other UI assets. Scripts / MoonDeck: - verify_version.py: accept the develop-on-a-prerelease release ritual — a stable vX.Y.Z tag matches library.json's core version with any -dev suffix dropped (v2.1.0 ↔ 2.1.0-dev passes; a wrong core still fails). latest still skips. Tests: - test/js/semver.test.mjs (new): pins parse + §11 precedence (core compare, release > prerelease, identifier precedence, v-strip, unparseable-sorts-lowest). - test/python/test_verify_version.py (new): pins the release ritual (v2.1.0 ↔ 2.1.0-dev OK, wrong core fails, latest skips). Docs / CI: - library.json: version 2.0.0 → 2.1.0-dev (the in-development prerelease). - FirmwareUpdateModule.md: version control description → pure semver, prerelease suffix marks a moving/pre-release build. - release.yml: add docs/install/** and docs/landing/** to the deploy paths so installer/landing changes auto-deploy (a prior eth-only fix didn't trigger a deploy because docs/install was missing). - Plan-20260624: saved approved plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (29)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/history/plans/Plan-20260624` - Semver version + update-available
badge.md:
- Around line 1-56: This plan note should not live under docs/history because
that area is meant for backward-looking, pruned history only. Move the content
to the appropriate active planning/docs location outside docs/history, and leave
docs/history for archived lessons only; update any references in the plan
document if needed so the new location is easy to find.
In `@src/ui/app.js`:
- Around line 2167-2170: The firmware update check is spawning duplicate
concurrent GitHub requests because updateStatusBar() repeatedly calls
checkFirmwareUpdate(false) before the cache is refreshed. Add in-flight
de-duplication inside checkFirmwareUpdate() and/or getLatestStableRelease() so
that when a fetch is already pending, subsequent callers reuse the same promise
instead of starting another releases/latest request. Make sure the shared
promise is cleared after it settles, and keep the cache-first behavior intact
for the updateStatusBar() refresh path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f3bf160c-9161-4a26-adcb-7553626cf46c
⛔ Files ignored due to path filters (1)
scripts/build/verify_version.pyis excluded by!**/build/**
📒 Files selected for processing (13)
.github/workflows/release.ymldocs/history/plans/Plan-20260624 - Semver version + update-available badge.mddocs/moonmodules/core/FirmwareUpdateModule.mdlibrary.jsonsrc/core/FirmwareUpdateModule.hsrc/core/HttpServerModule.cppsrc/ui/app.jssrc/ui/embed_ui.cmakesrc/ui/index.htmlsrc/ui/semver.jssrc/ui/style.csstest/js/semver.test.mjstest/python/test_verify_version.py
| # Plan — Semver-clean version + "firmware update available" badge | ||
|
|
||
| ## Context | ||
|
|
||
| The Firmware card's `version` control shows `2.0.0 (v2.0.0)` — a semver (`kVersion`) concatenated with a release-channel tag (`kRelease`). For a stable build the tag is just `v` + the semver, so it's redundant *and* non-semver. The product owner wants `version` to be **industry-standard semver, always** — and the channel **derivable from the semver itself**, not stored as separate metadata. The semver-correct way (semver.org §9/§11) to express "a moving `latest`/dev build that is ahead of the last stable but not itself a release" is a **prerelease identifier**: `2.1.0-dev`. So a stable build shows `2.0.0`; the moving `latest` build shows `2.1.0-dev`. Channel = "has a prerelease suffix → not stable." | ||
|
|
||
| On top of that clean version, add a status-bar **"firmware update available" badge**: the browser compares the device's running semver to the newest GitHub **stable** release and, when newer, shows a badge that opens the Firmware card. Modelled on ESP32-sveltekit's `UpdateIndicator.svelte` (the upstream firmware lineage MoonLight forks) — *carry the idea forward, write our own code* (CLAUDE.md *Industry standards, our own code*). | ||
|
|
||
| ## Git tag vs firmware version (important distinction) | ||
| `2.1.0-dev` is the **semver burned into the firmware** (`MM_VERSION`), NOT a new git tag. The moving build keeps its **`latest`** GitHub tag — only the version *inside* it changes. So: stable release → tag `v2.0.0`, firmware version `2.0.0`; moving build → tag `latest` (unchanged), firmware version `2.1.0-dev`; next stable → tag `v2.1.0`, firmware version `2.1.0` (the `-dev` suffix dropped at release time). The badge compares the device's firmware version against `releases/latest` (newest stable, `latest` excluded), so a `2.1.0-dev` device shows no badge — it is correctly *ahead* of the latest stable. | ||
|
|
||
| ## Decisions made with the PO | ||
| - Moving/latest builds carry **`2.1.0-dev`** (library.json bumped to the next dev version right after each release — standard "develop on a prerelease" flow). | ||
| - Badge fetches GitHub **cached in localStorage, re-fetch only if > 1 hour stale, PLUS** a fresh check when the Firmware module is opened (don't slow page load). | ||
| - Semver comparison via a **reusable `src/ui/semver.js`** (our own code, no npm dep), JS unit test. Improves the codebase's semver story (today releases sort by *date*; no semver compare exists). | ||
| - **Badge click → open the Firmware card with the new release pre-selected** (lands the user one click from Install). Reuses the picker's `PREF_RELEASE_KEY` restore + `selectModule()`; no new popup. | ||
|
|
||
| ## Approach (3 pieces) | ||
|
|
||
| ### 1. Semver-clean version (build pipeline + firmware) | ||
| - `library.json`: version `2.0.0` → `2.1.0-dev`. `build_info.h` is gitignored + generated from this, so `MM_VERSION` follows. | ||
| - `scripts/build/verify_version.py`: a stable `vX.Y.Z` tag matches `library.json` **with any prerelease suffix stripped** (so `v2.1.0` ↔ `2.1.0-dev` passes — the release of what was in dev; a wrong *core* like `v2.2.0` ↔ `2.1.0-dev` still fails). Keep the `latest`-skips behaviour. Doc the ritual. | ||
| - `src/core/FirmwareUpdateModule.h` (`setup()`): `version` control = **just `kVersion`** (pure semver). Drop the `(kRelease)` concatenation. Update inline comment + spec doc. | ||
| - `docs/moonmodules/core/FirmwareUpdateModule.md`: `version` description → "pure semver; a `-dev`/prerelease suffix marks a moving/pre-release build." | ||
|
|
||
| ### 2. Reusable semver module (`src/ui/semver.js`, NEW) | ||
| - Dependency-free, textbook: `parse(v)` (strip leading `v`) → `{major,minor,patch,prerelease[]}`; `compare(a,b)` → -1/0/1 per semver.org §11 (numeric core, then prerelease-present < absent, identifiers field-by-field, numeric < non-numeric); `isNewer(candidate,current)` = `compare===1`. | ||
| - One home for the comparison (CLAUDE.md *Complexity lives in core*). ESM, importable by app.js + the picker. | ||
|
|
||
| ### 3. "Update available" badge (status bar) | ||
| - `src/ui/index.html`: `<a id="fw-update-badge" class="fw-update-badge" hidden>` in `#status-bar`, before `#ws-dot`. | ||
| - `src/ui/style.css`: small amber-ish badge (reuse existing palette), hidden by default. | ||
| - `src/ui/app.js`: | ||
| - Cache + fetch (reuse picker's `safeLocalGet/Set` + TTL pattern; key `projectMM.update.latest.v1`; TTL 1 h; serve stale on failure). `getLatestStableRelease({force})` → fetches `api.github.com/repos/MoonModules/projectMM/releases/latest` only if stale or forced. | ||
| - Compare: device `version` + `firmware` key from `/api/state`; `isNewer(latest.tag_name, deviceVersion)` AND a compatible `firmware-<key>-v<ver>.bin` exists in the release assets (mirrors sveltekit's asset-target check). | ||
| - When: cache-first check on load; `{force:true}` when the Firmware module opens. | ||
| - Click: `safeLocalSet("projectMM.picker.releaseTag", tag)` then `selectModule(<firmware module>)` → Firmware card opens with the new release pre-selected, Install ready. | ||
| - Graceful: any failure → badge hidden, `console.warn` only. | ||
|
|
||
| ## Files | ||
| - `library.json` · `scripts/build/verify_version.py` · `src/core/FirmwareUpdateModule.h` · `docs/moonmodules/core/FirmwareUpdateModule.md` | ||
| - `src/ui/semver.js` (NEW) · `src/ui/index.html` · `src/ui/style.css` · `src/ui/app.js` | ||
| - `test/js/semver.test.mjs` (NEW) | ||
|
|
||
| ## Verification | ||
| - Host: `node --test "test/js/**/*.test.mjs"`; `node --check` the JS + extract-check index.html; `cmake --build build` + `ctest`; `uv run scripts/scenario/run_scenario.py`; `uv run scripts/check/check_specs.py`; a `test/python` verify_version case (`v2.1.0` ↔ `2.1.0-dev` OK; `v2.2.0` ↔ `2.1.0-dev` fails). | ||
| - Bench/preview: Firmware card shows clean semver; badge appears on an older device, opens Firmware pre-selected; no badge on newest stable; no error offline. | ||
|
|
||
| ## Existing releases — already semver-compatible (no migration) | ||
| Only `v1.0.0` + `v2.0.0` (both clean semver) + `latest` (moving prerelease channel, excluded by `releases/latest`). Badge input is always a clean `vX.Y.Z`; nothing to migrate. | ||
|
|
||
| ## Risks / notes | ||
| - Release ritual: next stable bumps `library.json` `2.1.0-dev` → `2.1.0` before tagging. Keep verify_version's suffix-strip exact so a wrong core still fails. Call out in the PR. | ||
| - GitHub rate limit (60/h/IP): 1 h cache + serve-stale keeps it well under; badge is best-effort. | ||
| - No npm toolchain: semver.js is plain ESM, `node:test` only. Our own code, no `compare-versions` dep. | ||
| - `release.yml` `paths:` change currently uncommitted on this branch is a *separate* installer-deploy fix — commit independently. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Move this plan out of docs/history.
This is active planning, but docs/history/**/*.md is meant to stay backward-looking and be pruned once the lesson is absorbed. Keeping PR plans here will make history notes grow indefinitely.
As per path instructions, docs/history/**/*.md: Keep history pages backward-looking and prune them when lessons have been absorbed; the permanent record is in git commits, not in indefinitely growing history notes.
🧰 Tools
🪛 LanguageTool
[style] ~6-~6: ‘On top of that’ might be wordy. Consider a shorter alternative.
Context: ...has a prerelease suffix → not stable." On top of that clean version, add a status-bar **"firm...
(EN_WORDINESS_PREMIUM_ON_TOP_OF_THAT)
[uncategorized] ~24-~24: Do not mix variants of the same word (‘pre-release’ and ‘prerelease’) within a single text.
Context: ...-dev/prerelease suffix marks a moving/pre-release build." ### 2. Reusable semver module ...
(EN_WORD_COHERENCY)
🪛 markdownlint-cli2 (0.22.1)
[warning] 9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 12-12: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 26-26: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 30-30: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 40-40: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 49-49: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 52-52: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/history/plans/Plan-20260624` - Semver version + update-available
badge.md around lines 1 - 56, This plan note should not live under docs/history
because that area is meant for backward-looking, pruned history only. Move the
content to the appropriate active planning/docs location outside docs/history,
and leave docs/history for archived lessons only; update any references in the
plan document if needed so the new location is easy to find.
Source: Path instructions
…r CSS/JS Moving `latest` firmware builds now carry a monotonic version (2.1.0-dev.<N>, N = commits since the last tag) instead of a static 2.1.0-dev, so a device on an older latest build can tell a newer latest exists. The status-bar update badge gains a dev channel: a device on a -dev build also lights up when a newer latest is available, not just for stable releases. Separately, the web installer's inline CSS and module JS are extracted into install.css + install.js to slim index.html. KPI: 16384lights | PC:383KB | tick:117/87/117/9/1/314/36/15/18/115/11us(FPS:8547/11494/8547/111111/1000000/3184/27777/66666/55555/8695/90909) | ESP32:1227KB | src:97(20052) | test:69(10600) | lizard:75w Core: - build_info.h (generate_build_info.py): MM_VERSION becomes an overridable #ifndef default (= library.json), so the pipeline can inject a precise per-build version, matching the existing MM_RELEASE / MM_FIRMWARE_NAME pattern. - esp32/main/CMakeLists.txt: forward -DMM_VERSION to the compiler (the missing target_compile_definitions that made the override a no-op until now). UI: - app.js: dev-channel update badge — a device on a -dev build checks the moving `latest` release's manifest version and badges when a newer latest exists; stable updates take precedence. Plus in-flight fetch de-duplication so the per-tick update check can't spawn duplicate concurrent GitHub requests on a cold cache. - semver.js tests: pin monotonic -dev.<N> ordering (dev.7 > dev.6, dev.1 > dev). Scripts / MoonDeck: - compute_version.py (new): computes the build version per channel — core semver for a stable tag, <core>-dev.<N> for a moving latest build (N = commits since the last v* tag, tag-less fallback to root count). - build_esp32.py: --version CLI arg → -DMM_VERSION; stale-feature-cache now also detects a changed MM_VERSION/MM_RELEASE value so a reused build dir can't silently ship the old version. Tests: - test_compute_version.py (new): per-channel version + core-strip + tag-less fallback. Docs / CI: - release.yml: compute the version once per build (stable core vs latest -dev.<N>), fetch-depth: 0 for git history, thread it into the binary, asset names, and manifest so all three agree. - FirmwareUpdateModule.md: document the per-build -dev.<N> version for latest builds. - docs/install: extract index.html's inline <style> and module <script> into install.css + install.js (2080 → 402 lines); both deploy paths (Pages cp -r, preview_installer suffix-glob) pick them up automatically. - Plan-20260624 - Dev-channel update badge: saved approved plan. Reviews: - 🐇 CodeRabbit: duplicate concurrent GitHub fetches on cold cache — fixed with in-flight de-dup in cachedJson (shared promise per cache slot, cleared on settle, cache-first preserved). - 🐇 CodeRabbit: move plan out of docs/history — declined; CLAUDE.md mandates docs/history/plans/ as the permanent plan archive (kept, not pruned), and ~20 plans already live there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/history/plans/Plan-20260624 - Dev-channel update badge.md (1)
1-49: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winMove this active plan out of
docs/history/.This page is a forward-looking implementation plan, but
docs/history/**is reserved for backward-looking notes that get pruned once the lessons are absorbed. Keeping live planning docs here pushes the history tree against the documented convention.As per coding guidelines, "Keep history pages backward-looking and prune them when lessons have been absorbed; the permanent record is in git commits, not in indefinitely growing history notes."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/history/plans/Plan-20260624` - Dev-channel update badge.md around lines 1 - 49, The plan document is forward-looking but lives under the history docs, which should only contain backward-looking notes. Move this plan out of docs/history/ and into the appropriate active planning/docs location, preserving the content and using the same document title if needed; update any references so the active plan is discoverable, and keep docs/history reserved for prunable retrospective notes.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 150-152: The release workflow currently maps every non-latest tag
to stable, so RC tags like v2.1.0-rc1 get the plain core version instead of an
RC-aware version. Update the version computation logic in the release job’s
compute-version step to branch explicitly for prerelease/RC tags the same way as
the other version step, and ensure it passes the correct channel into
scripts/build/compute_version.py or the uv run equivalent. Also add a test
alongside test/python/test_compute_version.py that asserts RC tags produce a
prerelease version rather than collapsing to stable X.Y.Z.
In `@docs/install/install.css`:
- Line 233: Replace the deprecated long-token wrapping rule in the stylesheet by
updating the .bd-val declaration to use overflow-wrap: anywhere instead of
word-break: break-word, and apply the same change in the other matching .bd-val
rule referenced by the review; keep the existing flex/min-width behavior intact
while adjusting the wrapping property.
In `@docs/install/install.js`:
- Around line 510-518: The success modal in install.js assigns clickable URLs in
the done-url and done-url-mdns elements before validating them, so a non-http(s)
result can still become an unsafe link. Update the success-link handling around
the code that sets a.href and aMdns.href to only assign href after the same
http(s)-only validation used by myDevices.addProvisionedDevice(), and skip or
neutralize rendering for invalid values.
- Around line 489-501: The success flow in install.js shows the “Device is
online!” header even when no device URL is available, so update the post-flash
handling around the no-URL branch to avoid setting that online state in the
first place. Adjust the logic in the success path and the no-url handling so
that when `url` is missing, the done section uses a neutral/offline message and
only shows the online header when a real device address exists; keep the
behavior localized to the `showSection("done")` / `done-url` / `done-defaults`
flow.
- Around line 876-883: The erase-only success handler is leaving the page in a
stale “done” state, so update the `onSuccess` flow to explicitly reset the
unload guard and clear any previous `done-status` content before showing the
erase completion message. Use the existing `onSuccess` callback in
`docs/install/install.js` and the `showSection("done")` / `done-url` /
`done-url-mdns` / `done-defaults` logic to locate the right spot, and make sure
the erase path does not inherit install-only status or tab-close warnings.
In `@src/ui/app.js`:
- Around line 2274-2275: The dev manifest cache key is shared across firmware
variants even though the URL in the manifest fetch is firmware-specific, so
update the cachedJson call in the dev update path to include dev.firmware in the
cache slot key. Use the existing manifest-loading logic around the url/manifest
assignment in app.js to make the cache entry unique per firmware so one variant
cannot reuse another variant’s latest manifest state.
- Around line 2264-2266: The release asset lookup in the update matching logic
only checks the tagged version form, so stable binaries named with the cleaned
semver are missed. Update the asset-name check in the matching flow around the
version comparison to also consider the pure version derived from rel.tag_name
(without the leading v) when evaluating hasBinary, using the existing release
selection logic and identifiers like isNewer, rel.tag_name, dev.version, and
assetNames.
- Around line 2225-2229: The stale-cache fallback in update should also refresh
the cache metadata so it does not keep retrying on every status tick. In the
catch block that serves data from safeLocalGet(key), update the stored
timestamp/expiry for that key before returning the parsed cached payload, using
the same cache entry logic already used by the successful fetch path in update
so the fallback remains valid until the next intended refresh.
In `@test/python/test_compute_version.py`:
- Around line 28-48: The version computation tests do not cover the no-tag
fallback path promised by the docstring. Add a test in test_compute_version.py
that exercises compute() or the underlying helper when no v* tag exists,
verifying it falls back to rev-list --count HEAD behavior for the first-release
case; use monkeypatch on the relevant helper(s) like commits_since_last_stable
and the version source via LIBRARY_JSON to keep the test deterministic.
---
Outside diff comments:
In `@docs/history/plans/Plan-20260624` - Dev-channel update badge.md:
- Around line 1-49: The plan document is forward-looking but lives under the
history docs, which should only contain backward-looking notes. Move this plan
out of docs/history/ and into the appropriate active planning/docs location,
preserving the content and using the same document title if needed; update any
references so the active plan is discoverable, and keep docs/history reserved
for prunable retrospective notes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4cd67847-7e82-47c2-a78f-d5f8921ecfb5
⛔ Files ignored due to path filters (3)
scripts/build/build_esp32.pyis excluded by!**/build/**scripts/build/compute_version.pyis excluded by!**/build/**scripts/build/generate_build_info.pyis excluded by!**/build/**
📒 Files selected for processing (11)
.github/workflows/release.ymldocs/history/plans/Plan-20260624 - Dev-channel update badge.mddocs/install/index.htmldocs/install/install.cssdocs/install/install.jsdocs/moonmodules/core/FirmwareUpdateModule.mdesp32/main/CMakeLists.txtsrc/ui/app.jstest/js/semver.test.mjstest/python/test_compute_version.pytest/scenarios/light/scenario_Audio_mutation.json
| const hasBinary = !dev.firmware || | ||
| assetNames.some(n => n === `firmware-${dev.firmware}-${rel.tag_name}.bin`); | ||
| return (isNewer(rel.tag_name, dev.version) && hasBinary) ? rel.tag_name : null; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Match stable binaries by clean semver, not only the v tag.
If release assets are named with the computed pure version, firmware-${dev.firmware}-${rel.tag_name}.bin looks for ...-v2.1.0.bin while the asset is ...-2.1.0.bin, so compatible updates never show.
Proposed fix
const assetNames = (rel.assets || []).map(a => a.name);
+ const releaseVersion = rel.tag_name.replace(/^v/, "");
const hasBinary = !dev.firmware ||
- assetNames.some(n => n === `firmware-${dev.firmware}-${rel.tag_name}.bin`);
+ assetNames.some(n =>
+ n === `firmware-${dev.firmware}-${releaseVersion}.bin` ||
+ n === `firmware-${dev.firmware}-${rel.tag_name}.bin`
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hasBinary = !dev.firmware || | |
| assetNames.some(n => n === `firmware-${dev.firmware}-${rel.tag_name}.bin`); | |
| return (isNewer(rel.tag_name, dev.version) && hasBinary) ? rel.tag_name : null; | |
| const assetNames = (rel.assets || []).map(a => a.name); | |
| const releaseVersion = rel.tag_name.replace(/^v/, ""); | |
| const hasBinary = !dev.firmware || | |
| assetNames.some(n => | |
| n === `firmware-${dev.firmware}-${releaseVersion}.bin` || | |
| n === `firmware-${dev.firmware}-${rel.tag_name}.bin` | |
| ); | |
| return (isNewer(rel.tag_name, dev.version) && hasBinary) ? rel.tag_name : null; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ui/app.js` around lines 2264 - 2266, The release asset lookup in the
update matching logic only checks the tagged version form, so stable binaries
named with the cleaned semver are missed. Update the asset-name check in the
matching flow around the version comparison to also consider the pure version
derived from rel.tag_name (without the leading v) when evaluating hasBinary,
using the existing release selection logic and identifiers like isNewer,
rel.tag_name, dev.version, and assetNames.
…re + MoonLive analysis Adds a dev-channel to the update-available badge (a device on a -dev build is nudged when a newer latest exists, with per-build versions like 2.1.0-dev.<N>), extracts the web installer's inline CSS/JS into separate files, restructures the backlog into core/light/mixed with the README as the single landing page, and lands the MoonLive live-script-engine analysis (bottom-up survey + top-down design). KPI: 16384lights | PC:383KB | tick:119/87/117/9/1/317/37/15/18/117/11us(FPS:8403/11494/8547/111111/1000000/3154/27027/66666/55555/8547/90909) | ESP32:1228KB | src:97(20052) | test:69(10600) | lizard:75w UI: - app.js: dev-channel update badge — a device on a -dev build checks the moving `latest` release's manifest version and badges when a newer latest exists (stable updates take precedence); per-firmware dev cache key; the stale-cache fallback refreshes its timestamp so a failing fetch backs off instead of retrying every tick. Scripts / MoonDeck: - compute_version.py: an -rc tag now yields its own prerelease semver instead of collapsing to the core version (was mislabeling RC binaries as stable). - build_esp32.py / setup_esp_idf.py: minor doc-link updates to the restructured backlog. Tests: - test_compute_version.py: cover the -rc-tag case and the no-tag (first-release) commit-count fallback. Docs / CI: - docs/install: extract index.html's inline CSS + module JS into install.css + install.js, fold the Windows bootstrap into the module; .bd-val wrapping uses overflow-wrap: anywhere; success-link href validated http(s) only; "Device is online!" no longer shown on the no-URL / erase paths; erase onSuccess resets the unload guard + done-status. - docs/backlog: split backlog.md into backlog-core.md / backlog-light.md / backlog-mixed.md (folding ui-deferred + leddriver-deferred in), README becomes the landing page, inbound links across the docs/scripts repointed to it; add the MoonLive live-script-engine analysis (livescripts-analysis-bottom-up.md survey + livescripts-analysis-top-down.md design, with prior-art credit to hpwit/ESPLiveScript and ewowi/ARTI-FX). - architecture.md: present-tense pass (state what is, not what isn't) on the modifier/effect description. - release.yml: compute the build version once per channel (stable core, latest <core>-dev.<N>, rc keeps its prerelease), fetch-depth 0, thread it into the binary + asset names + manifest; add docs/install + docs/landing to the deploy paths. - CLAUDE.md / building.md / history + moonmodules specs: backlog link updates for the new structure. Reviews: - 🐇 CodeRabbit: RC-tag version, .bd-val wrapping, success-link validation, online-header on no-url, erase onSuccess state, per-firmware dev cache key, stale-cache fallback ts — all fixed. - 🐇 CodeRabbit: hasBinary asset-name match — skipped, the existing v-prefixed match is already correct (verified against v2.0.0 assets). - 🐇 CodeRabbit: move plan out of docs/history — skipped, CLAUDE.md mandates docs/history/plans/ as the permanent plan archive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.github/workflows/release.yml (2)
146-153: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftCentralize the tag→version resolution.
These two jobs still duplicate the same release-tag/channel/version mapping. The next semver-rule edit can land in one copy and silently desync the binary
MM_VERSION, asset filenames, and manifest--version. Move this logic behind one checked-in helper and call it from both places.Based on learnings, "Do not duplicate logic or facts across code or documentation; shared behavior belongs in shared functions, and repeated facts in docs should be consolidated and linked instead of restated."
Also applies to: 304-311
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release.yml around lines 146 - 153, The release-tag to version mapping is duplicated in the workflow, which can desync version-related outputs; centralize this logic in one checked-in helper and reuse it from the Compute version step and the other matching job so binary MM_VERSION, asset filenames, and manifest --version all come from the same source. Keep the shared behavior behind a single helper script/function (for example the existing compute_version.py path or a new wrapper) and update both workflow spots to call it instead of re-implementing the channel/tag branching inline.Source: Learnings
146-152: 📐 Maintainability & Code Quality | 🔵 TrivialReplace raw
pythonwithuv runin the compute version step.The "Compute version" step at line 152 invokes
pythondirectly, whereas line 310 in the same workflow correctly usesuv run pythonfor the same script. Unify these execution paths to align with repository standards and ensure consistent runtime environments.Suggested change
set -euo pipefail if [ "${{ steps.tag.outputs.tag }}" = "latest" ]; then CH=latest; else CH=stable; fi # Pass the tag so an -rc tag yields its own prerelease semver (not the core). - V=$(python scripts/build/compute_version.py --channel "$CH" --tag "${{ steps.tag.outputs.tag }}") + V=$(uv run python scripts/build/compute_version.py --channel "$CH" --tag "${{ steps.tag.outputs.tag }}")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release.yml around lines 146 - 152, The Compute version step still calls the compute_version.py script with raw python, which should be aligned with the rest of the workflow. Update the run block in the ver step to use uv run python for scripts/build/compute_version.py, matching the existing execution pattern used elsewhere in this workflow and keeping the environment consistent.Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/architecture.md`:
- Around line 354-356: Update the modifier wording in the architecture docs so
it does not imply chained modifier execution is implemented today: in the
section describing Layer and Layer::rebuildLUT, keep the contract that only the
first enabled modifier is applied, and either remove the sequencing examples or
clearly label them as future/backlog behavior. Use the existing symbols
Layer::rebuildLUT, modifiers, and backlog/README.md to keep the text aligned
with current behavior.
In `@docs/backlog/livescripts-analysis-top-down.md`:
- Around line 372-373: Update the Stage 0.5 acceptance text in the backlog doc
so it validates seam correctness by observable behavior rather than requiring
front-end and IR files to be byte-identical. Keep the existing references to the
same input, same front-end/IR path, and same rendered output, but replace the
file-identity requirement with a behavior-based proof that both backends (RISC-V
P4 and desktop x86-64) produce equivalent results.
- Line 427: The current wording conflates the tutorial hello-world spike with
the fix-warnings crash repro; revise the sentence in the
livescripts-analysis-top-down doc to keep them clearly separate. Update the text
around the Ripples comparison and the `setRGB(random16, blue)` reference so it
says the hello-world script is the tutorial spike, while the `fix-warnings` fork
is the distinct regression case. Use the existing terms `Ripples`,
`setRGB(random16, blue)`, and `fix-warnings` to anchor the distinction.
- Around line 216-219: The safety-tier section conflates runtime protections
with the parser crash from `fix-warnings`; update the discussion around the
nested-arg null-deref to separate AST/parsing validation from runtime bounds
checks and the watchdog. In the `livescripts-analysis-top-down` content, call
out that missing AST children must be handled by parser/analysis validation and
backed by a regression test, while keeping the runtime tier limited to `setRGB`,
indexed access, and watchdog-style protections.
---
Outside diff comments:
In @.github/workflows/release.yml:
- Around line 146-153: The release-tag to version mapping is duplicated in the
workflow, which can desync version-related outputs; centralize this logic in one
checked-in helper and reuse it from the Compute version step and the other
matching job so binary MM_VERSION, asset filenames, and manifest --version all
come from the same source. Keep the shared behavior behind a single helper
script/function (for example the existing compute_version.py path or a new
wrapper) and update both workflow spots to call it instead of re-implementing
the channel/tag branching inline.
- Around line 146-152: The Compute version step still calls the
compute_version.py script with raw python, which should be aligned with the rest
of the workflow. Update the run block in the ver step to use uv run python for
scripts/build/compute_version.py, matching the existing execution pattern used
elsewhere in this workflow and keeping the environment consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 08dea03e-dd73-42d9-af57-84122ab14a98
⛔ Files ignored due to path filters (3)
scripts/build/build_esp32.pyis excluded by!**/build/**scripts/build/compute_version.pyis excluded by!**/build/**scripts/build/setup_esp_idf.pyis excluded by!**/build/**
📒 Files selected for processing (26)
.github/workflows/release.ymlCLAUDE.mddocs/architecture.mddocs/backlog/README.mddocs/backlog/backlog-core.mddocs/backlog/backlog-light.mddocs/backlog/backlog-mixed.mddocs/backlog/leddriver-deferred.mddocs/backlog/livescripts-analysis-bottom-up.mddocs/backlog/livescripts-analysis-top-down.mddocs/backlog/ui-deferred.mddocs/building.mddocs/history/README.mddocs/history/v1-inventory.mddocs/install/install.cssdocs/install/install.jsdocs/moonmodules/core/AudioModule.mddocs/moonmodules/light/BlendMap.mddocs/moonmodules/light/Drivers.mddocs/moonmodules/light/Layers.mddocs/moonmodules/light/drivers/LcdLedDriver.mddocs/moonmodules/light/drivers/NetworkSendDriver.mddocs/performance.mdsrc/ui/app.jstest/python/test_compute_version.pytest/scenarios/light/scenario_Audio_mutation.json
💤 Files with no reviewable changes (2)
- docs/backlog/leddriver-deferred.md
- docs/backlog/ui-deferred.md
| A layer can have **multiple effects**. Each effect writes to the buffer sequentially in its listed order, overwriting or adding to the previous — so the effects stack (a base-colour effect followed by a sparkle effect). | ||
|
|
||
| A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifier *chaining* (applying several in sequence) is not implemented: only the first enabled modifier takes effect. Order matters for a chain (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror), which is why modifiers are reorderable in the UI even though only the first is applied today. Chaining is on the [backlog](backlog/backlog.md): static modifiers chain during LUT build, dynamic modifiers during rendering. | ||
| A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md). |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify that modifier chaining is still backlog-only.
The new example reads like multiply-then-checkerboard vs checkerboard-then-multiply already changes behavior, but the current contract still applies only the first enabled modifier during Layer::rebuildLUT. Please remove the sequencing example or mark it explicitly as future behavior.
♻️ Suggested rewording
-A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md).
+A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI so users can choose which one is active first. Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| A layer can have **multiple effects**. Each effect writes to the buffer sequentially in its listed order, overwriting or adding to the previous — so the effects stack (a base-colour effect followed by a sparkle effect). | |
| A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifier *chaining* (applying several in sequence) is not implemented: only the first enabled modifier takes effect. Order matters for a chain (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror), which is why modifiers are reorderable in the UI even though only the first is applied today. Chaining is on the [backlog](backlog/backlog.md): static modifiers chain during LUT build, dynamic modifiers during rendering. | |
| A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI, and order is meaningful (a multiply-then-checkerboard mask differs from checkerboard-then-multiply, just as mirror-then-rotate differs from rotate-then-mirror). Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md). | |
| A layer can have **multiple effects**. Each effect writes to the buffer sequentially in its listed order, overwriting or adding to the previous — so the effects stack (a base-colour effect followed by a sparkle effect). | |
| A layer applies its **first enabled modifier** during LUT build (`Layer::rebuildLUT`). Modifiers are **reorderable** in the UI so users can choose which one is active first. Applying several modifiers in sequence (chaining) is on the [backlog](backlog/README.md). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/architecture.md` around lines 354 - 356, Update the modifier wording in
the architecture docs so it does not imply chained modifier execution is
implemented today: in the section describing Layer and Layer::rebuildLUT, keep
the contract that only the first enabled modifier is applied, and either remove
the sequencing examples or clearly label them as future/backlog behavior. Use
the existing symbols Layer::rebuildLUT, modifiers, and backlog/README.md to keep
the text aligned with current behavior.
Layers now composite: each enabled Layer renders its own buffer and the Drivers container blends them bottom-to-top into the output buffer, each layer per its own blend mode (alpha-over or additive) and opacity. A single layer keeps the existing zero-copy fast path. Tested live on classic ESP32 (Olimex), S3, and P4. Also fixes two web-UI bugs the feature surfaced: a control-row rebuild loop that broke dropdowns on nested cards, and a firmware-update-check CORS error storm. KPI: 16384lights | PC:383KB | tick:114/87/117/9/1/313/36/15/18/53/113/11us(FPS:8771/11494/8547/111111/1000000/3194/27777/66666/55555/18867/8849/90909) | ESP32:1230KB | tick:4168us(FPS:239) | heap:8335KB | src:97(20197) | test:69(10922) | lizard:76w Light domain: - BlendMap: added a BlendOp enum (Overwrite/Alpha/Additive) plus opacity and clearFirst params; alpha-over uses textbook 8-bit integer math (src*a + dst*(255-a))/255 via the (x+(x>>8)+1)>>8 reciprocal, additive sums with clamp. Both the LUT path and the no-LUT (dense-grid 1:1) path blend per op/opacity. A default Overwrite still defers to the LUT's overwrites() flag so within-layer self-overlaps keep accumulating. - Layer: added blendMode (alpha/additive select) and opacity (0-255) controls. They are inert on the Layer (it never reads them); Drivers reads them plus the container child order and does the compositing. Precedent: the value-on-X, logic-in-Drivers split used for Correction. - Layers: added enabledLayerCount() and forEachEnabledLayer(cb) (ordered bottom-to-top walk, isFirst marks the bottom layer that clears the buffer); kept activeLayer() for dimensions and the single-layer fast path. - Drivers: composite loop over enabled layers into outputBuffer_ (first overwrites/clears, each above blends per its mode+opacity); output buffer allocated only when compositing (>=2 layers) or a layer has a LUT, else drivers read the lone layer's buffer directly. Single-enabled-layer path is byte-for-byte the previous pipeline. UI: - app.js: fixed syncVisibleControls resolving its controls host with a descendant query, so a container card (Layers) adopted a nested child card's (Layer) control rows; the two cards then rebuilt each other's rows every WS frame, destroying any open <select>. Scoped to :scope > .card-controls-collapse. Guarded an open/focused select from WS value patches (data-open flag) and dropped the post-change refetchState that collapsed expanded cards. Negative-cache failed firmware-update fetches so a CORS-blocked check backs off for the TTL instead of refetching ~4x/sec (a console error storm); downgraded our own log to console.debug. Tests: - unit_BlendMap: added alpha/additive/opacity/clamp/endpoint cases on both the LUT and the no-LUT branch (the dense-grid path that runs on real hardware). - unit_Layers_container: Drivers composites two enabled layers into one output buffer (asserts the blended bytes), drops cleanly to a single layer on disable, and allocates the output buffer only when compositing or mapping is needed (dynamicBytes contract across the three cases: identity->none, two layers->buffer, LUT->buffer). - scenario_Layers_composition: two-layer pipeline liveness + bounded FPS. Docs / CI: - architecture.md: multi-layer composition section made present-tense; corrected the output-buffer allocation rule to include the >=2-layers trigger. - Layer/Layers/Drivers/BlendMap specs: documented the controls + compositing wiring. Backlog: removed the shipped composition item (left per-Layer region carving), noted the low-heap degradation-cascade test gap. - compute_version.py / release.yml: centralised the tag->channel mapping in channel_for_tag() so the workflow's two compute-version steps can't disagree; --channel auto derives it from --tag (from an earlier CodeRabbit review batch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Created PR with unit tests: #28 |
Summary
Makes the firmware version industry-standard semver, gives every build a precise identity, and surfaces available updates in the UI — for both stable releases and the moving
latestchannel. Also slims the web installer page.1. Semver-clean version. The Firmware card's
versionwas2.0.0 (v2.0.0)— a semver concatenated with a redundant, non-semver channel tag. It's now pure semver:2.0.0on a stable release; the channel is derivable from the prerelease suffix, no longer mixed in.library.jsonmoves to2.1.0-dev(develop-on-a-prerelease), andverify_version.pyaccepts the release ritual (avX.Y.Ztag releases what wasX.Y.Z-dev).2. Per-build
latestversion. A movinglatestbuild had no distinct identity — every one reported the same version, and its manifest/assets were stamped with the old stable version. Now eachlatestbuild carries a monotonic2.1.0-dev.<N>(N = commits since the lastv*tag, viacompute_version.py), threaded consistently through the binary (MM_VERSION), asset names, and manifest. semver.org §11 orders these numerically.3. Update-available badge (two channels). A status-bar badge, modelled on ESP32-sveltekit's
UpdateIndicator(our own code, no npm dep):releases/latest, prereleases excluded).-devbuild also checks the movinglatestrelease's manifest version, so a stale latest build is nudged to the newest latest.Cached in localStorage (1 h TTL) so it doesn't slow page load, forced fresh when the Firmware card opens, in-flight de-duplicated. Click pre-selects the release in the install picker and opens the Firmware card — one click from installing. Best-effort: any failure hides the badge.
4. Installer CSS/JS extraction.
docs/install/index.html(2080 → 402 lines): inline<style>→install.css, inline module<script>→install.js(consistent with the sibling modules it already imports).A new dependency-free
src/ui/semver.js(parse + §11-correct compare +isNewer) is the one home for version comparison.Testing
-Werror), ctest, scenarios, platform boundary, ESP32 build, KPI, firmware-list, host JS (15) + Python (15).versionreports clean semver and injected2.1.0-dev.<N>(proves the-DMM_VERSIONCMake wiring); stable badge appears + opens Firmware pre-selected;/semver.jsserved from the device.preview_installer(index.html + install.css + install.js + imported modules all serve 200).Release note
The next stable release must bump
library.json2.1.0-dev→2.1.0before tagging (drop the-devsuffix). The dev-channel badge becomes fully testable once alatestbuild republishes with the new2.1.0-dev.<N>manifest (today'slateststill carries the pre-change2.0.0).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes