feat: roll back to stable firmware from device info page#6296
Conversation
…/stable endpoint Extract version filtering, prefix mapping, and response building into reusable helpers. Add new /v2/firmware/stable endpoint that returns the latest stable firmware for a device regardless of current version, enabling rollback from custom firmware.
Add isRollback flag that fetches from stable endpoint and uses rollback-specific UI text for title, loading, and install button.
Shows only when device firmware differs from latest stable version.
Greptile SummaryThis PR adds a "Roll Back to Stable Firmware" option in the device info page, backed by a new Confidence Score: 4/5Safe to merge with minor cleanup — no blocking logic errors, but the stable endpoint can return a pre-release tag as "stable" firmware and three l10n keys are defined but never used. Both findings are P2: the pre-release leak is a correctness concern for the new "stable" endpoint (though it mirrors the pre-existing behavior of /v2/firmware/latest), and the unused l10n keys indicate a confirmation dialog was designed but not wired up. Neither blocks the primary user path, but the pre-release issue is worth addressing before shipping the stable endpoint broadly. backend/routers/firmware.py (_find_candidate_releases pre-release filtering) and app/lib/l10n/app_en.arb (unused rollbackConfirmTitle/Message/alreadyOnStableFirmware keys) Important Files Changed
Sequence DiagramsequenceDiagram
participant App as Flutter App
participant DP as DeviceProvider
participant BE as Backend
participant GH as GitHub Releases
App->>DP: connect device
DP->>BE: GET /v2/firmware/latest
BE->>GH: fetch releases (cached 5min)
GH-->>BE: releases[]
BE-->>DP: {version, zip_url, ...}
DP->>BE: GET /v2/firmware/stable?device_model=X
BE->>GH: fetch releases (cache hit)
GH-->>BE: releases[]
BE-->>DP: {version, zip_url, ...}
DP-->>App: latestStableFirmwareVersion set
Note over App: Show rollback button if firmwareRevision != latestStableFirmwareVersion
App->>App: navigate FirmwareUpdate(isRollback=true)
App->>BE: GET /v2/firmware/stable?device_model=X
BE-->>App: stable firmware details
App-->>App: show Install Stable Firmware button
App->>App: DFU install (same flow as regular update)
Reviews (1): Last reviewed commit: "chore(l10n): regenerate localization dar..." | Re-trigger Greptile |
| "noStableFirmwareFound": "Could not find a stable firmware version for your device.", | ||
| "@noStableFirmwareFound": { | ||
| "description": "Error message when no stable firmware is available" | ||
| }, | ||
| "installStableFirmware": "Install Stable Firmware", | ||
| "@installStableFirmware": { | ||
| "description": "Button text to install the stable firmware" | ||
| }, | ||
| "alreadyOnStableFirmware": "You are already on the latest stable version.", | ||
| "@alreadyOnStableFirmware": { | ||
| "description": "Message when device is already on the latest stable firmware" | ||
| } | ||
| } |
There was a problem hiding this comment.
Three l10n keys defined but never used in code
rollbackConfirmTitle, rollbackConfirmMessage, and alreadyOnStableFirmware are translated across all 34 locales but are never referenced anywhere in the Dart UI code (confirmed via search). rollbackConfirmTitle/rollbackConfirmMessage suggest a confirmation dialog before rollback was planned but never wired up — tapping "Roll Back to Stable Firmware" in device.dart navigates directly to FirmwareUpdate with no pre-flight confirmation. Either implement the dialog using these keys (which would be better UX before a DFU install) or remove the dead strings to avoid localization bloat.
| @router.get("/v2/firmware/stable") | ||
| async def get_stable_version(device_model: str): | ||
| """Return the latest stable firmware for a device, regardless of current version. | ||
|
|
||
| Used for rolling back to the official stable firmware after flashing custom firmware. | ||
| """ | ||
| device = _get_device_by_model_number(device_model) | ||
| if not device: | ||
| raise HTTPException(status_code=404, detail="Device not found") | ||
|
|
||
| releases = await get_omi_github_releases("github_releases_omi", tag_filter=FIRMWARE_TAG_PATTERN) | ||
| if not releases: | ||
| raise HTTPException(status_code=404, detail="No releases found for the repository") | ||
|
|
||
| release_prefix = _get_release_prefix(device) | ||
| candidates = _find_candidate_releases(releases, release_prefix) | ||
|
|
||
| if not candidates: | ||
| raise HTTPException(status_code=404, detail="No stable firmware found for your device.") | ||
|
|
||
| candidates.sort(key=lambda r: r.get("published_at", ""), reverse=True) | ||
| return _extract_firmware_response(device, candidates[0]) | ||
|
|
There was a problem hiding this comment.
Stable endpoint may return a GitHub pre-release
_find_candidate_releases only skips draft releases and those missing published_at or tag_name. It does not check the prerelease field from the GitHub API. If the most recently published release matching the prefix is a pre-release (e.g. a beta or RC), the /v2/firmware/stable endpoint would return it as "stable firmware" for rollback. The existing /v2/firmware/latest shares this gap, but the stakes are higher here because the endpoint is specifically branded "stable". Consider adding if release.get("prerelease"): continue inside _find_candidate_releases when current_firmware_tuple is None (or unconditionally if the intent is to treat all non-draft, non-pre-release tags as stable).
Uses rollbackConfirmTitle and rollbackConfirmMessage l10n keys.
Keep both rollback firmware keys and main's new audioSavedLocally/willSyncAutomatically keys. Regenerate localization dart files.
…re#6296) ## Summary - **Backend**: Refactored `/v2/firmware/latest` to extract shared logic into helpers (`_get_release_prefix`, `_find_candidate_releases`, `_extract_firmware_response`). Added new `/v2/firmware/stable` endpoint that returns the latest stable firmware for a device model regardless of current version. - **App**: Added "Roll Back to Stable Firmware" action in the device info page (between Product Update and SD Card Sync). Only visible when the connected device's firmware version differs from the latest stable version. - **Firmware Update page**: Accepts `isRollback` flag — when true, fetches from the stable endpoint and always allows installation with rollback-specific UI text. - **L10n**: 8 new keys with real translations across all 34 locales. ## Test plan - [x] Connect a device running custom/non-stable firmware → verify "Roll Back to Stable Firmware" option appears in device info - [ ] Connect a device running the latest stable firmware → verify rollback option does NOT appear - [x] Tap rollback → verify it fetches stable firmware details and shows install button - [x] Complete rollback install → verify DFU process works same as regular firmware update - [x] Verify `/v2/firmware/latest` still works unchanged for regular update checks - [x] Verify `/v2/firmware/stable?device_model=Omi+DevKit+2` returns latest stable release 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
/v2/firmware/latestto extract shared logic into helpers (_get_release_prefix,_find_candidate_releases,_extract_firmware_response). Added new/v2/firmware/stableendpoint that returns the latest stable firmware for a device model regardless of current version.isRollbackflag — when true, fetches from the stable endpoint and always allows installation with rollback-specific UI text.Test plan
/v2/firmware/lateststill works unchanged for regular update checks/v2/firmware/stable?device_model=Omi+DevKit+2returns latest stable release🤖 Generated with Claude Code