-
-
Notifications
You must be signed in to change notification settings - Fork 118
Upgrade reporting #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgrade reporting #286
Changes from all commits
6aaab57
d473263
d2dab32
23864c7
a967043
260f26d
f423710
00e026c
23ce580
aff7bbf
de9c6ae
0de55bd
0bcece3
49bd630
aa8148e
9a4e7a6
520bc8f
47a1b62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,3 +27,4 @@ compile_commands.json | |
| /wled00/Release | ||
| /wled00/wled00.ino.cpp | ||
| /wled00/html_*.h | ||
| _codeql_detected_source_root | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -717,11 +717,11 @@ ${inforow("Uptime",getRuntimeStr(i.uptime))} | |
| ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB, " +Math.round(i.fs.u*100/i.fs.t) + "%")} | ||
| ${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB",", "+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%"):inforow("Free heap",heap," kB")} <!--WLEDMM different for 8266--> | ||
| ${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(0)+" kB",", "+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%"):""} | ||
| ${i.psram?inforow("PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(0)+"/"+(i.tpram/1024).toFixed(0)+" kB",", "+((i.tpram-i.psram)*100.0/i.tpram).toFixed(1)+"%"):""} | ||
| ${i.psusedram?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpram-i.psusedram)*100.0/i.tpram).toFixed(1)+"%"):""} | ||
| ${i.psram?inforow("PSRAM ☾",((i.tpsram-i.psram)/1024).toFixed(0)+"/"+(i.tpsram/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psram)*100.0/i.tpsram).toFixed(1)+"%"):""} | ||
| ${i.psusedram?inforow("Max used PSRAM ☾",((i.tpsram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psusedram)*100.0/i.tpsram).toFixed(1)+"%"):""} | ||
| ${i.freestack?inforow("Free stack ☾",(i.freestack/1000).toFixed(3)," kB"):""} <!--WLEDMM--> | ||
| <tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr> | ||
| ${i.tpram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")} | ||
| ${i.tpsram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpsram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")} | ||
| ${i.e32flash?inforow("Flash mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flash+" MB, "+i.e32flashspeed," Mhz"):""} | ||
| ${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s),"," "+i.e32speed+" Mhz"):""} | ||
| ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} | ||
|
|
@@ -2229,6 +2229,7 @@ function requestJson(command=null) | |
| if (json.info) { | ||
| let i = json.info; | ||
| parseInfo(i); | ||
| checkVersionUpgrade(i); // Check for version upgrade | ||
| populatePalettes(i); | ||
| if (isInfo) populateInfo(i); | ||
| } | ||
|
|
@@ -3686,6 +3687,184 @@ function mergeDeep(target, ...sources) | |
| } | ||
| return mergeDeep(target, ...sources); | ||
| } | ||
| // Version reporting feature | ||
| var versionCheckDone = false; | ||
|
|
||
| function checkVersionUpgrade(info) { | ||
| // Only check once per page load | ||
| if (versionCheckDone) return; | ||
| versionCheckDone = true; | ||
|
|
||
| // Fetch version-info.json using existing /edit endpoint | ||
| fetch('/edit?edit=/version-info.json', { | ||
| method: 'get' | ||
| }) | ||
| .then(res => { | ||
| if (res.status === 404) { | ||
| // File doesn't exist - first install, show install prompt | ||
| showVersionUpgradePrompt(info, null, info.ver); | ||
| return null; | ||
| } | ||
| if (!res.ok) { | ||
| throw new Error('Failed to fetch version-info.json'); | ||
| } | ||
| return res.json(); | ||
| }) | ||
| .then(versionInfo => { | ||
| if (!versionInfo) return; // 404 case already handled | ||
|
|
||
| // Check if user opted out | ||
| if (versionInfo.neverAsk) return; | ||
|
|
||
| // Check if version has changed | ||
| const currentVersion = info.ver; | ||
| const storedVersion = versionInfo.version || ''; | ||
|
|
||
| if (storedVersion && storedVersion !== currentVersion) { | ||
| // Version has changed, show upgrade prompt | ||
| showVersionUpgradePrompt(info, storedVersion, currentVersion); | ||
| } else if (!storedVersion) { | ||
| // Empty version in file, show install prompt | ||
| showVersionUpgradePrompt(info, null, currentVersion); | ||
| } | ||
| }) | ||
| .catch(e => { | ||
| console.log('Failed to load version-info.json', e); | ||
| }); | ||
| } | ||
|
|
||
| function showVersionUpgradePrompt(info, oldVersion, newVersion) { | ||
| // Determine if this is an install or upgrade | ||
| const isInstall = !oldVersion; | ||
|
|
||
| // Create overlay and dialog | ||
| const overlay = d.createElement('div'); | ||
| overlay.id = 'versionUpgradeOverlay'; | ||
| overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; | ||
|
|
||
| const dialog = d.createElement('div'); | ||
| dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);'; | ||
|
|
||
| // Build contextual message based on install vs upgrade | ||
| const title = isInstall | ||
| ? '🎉 Thank you for installing WLED-MM!' | ||
| : '🎉 WLED-MM Upgrade Detected!'; | ||
|
|
||
| const description = isInstall | ||
| ? `You are now running WLED-MM <strong>${newVersion}</strong>.` | ||
| : `Your WLED-MM has been upgraded from <strong>${oldVersion}</strong> to <strong>${newVersion}</strong>.`; | ||
|
|
||
| const question = 'Would you like to help the WLED development team by reporting your installation? This helps us understand what hardware and versions are being used.' | ||
|
|
||
| dialog.innerHTML = ` | ||
| <h2 style="margin-top:0;color:var(--c-f);">${title}</h2> | ||
| <p style="color:var(--c-f);">${description}</p> | ||
| <p style="color:var(--c-f);">${question}</p> | ||
| <div style="margin-top:20px;"> | ||
| <button id="versionReportYes" class="btn">Yes</button> | ||
| <button id="versionReportNo" class="btn">Not Now</button> | ||
| <button id="versionReportNever" class="btn">Never Ask</button> | ||
| </div> | ||
| `; | ||
|
|
||
| overlay.appendChild(dialog); | ||
| d.body.appendChild(overlay); | ||
|
|
||
| // Add event listeners | ||
| gId('versionReportYes').addEventListener('click', () => { | ||
| reportUpgradeEvent(oldVersion, newVersion); | ||
| d.body.removeChild(overlay); | ||
| }); | ||
|
|
||
| gId('versionReportNo').addEventListener('click', () => { | ||
| // Don't update version, will ask again on next load | ||
| d.body.removeChild(overlay); | ||
| }); | ||
netmindz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| gId('versionReportNever').addEventListener('click', () => { | ||
| updateVersionInfo(newVersion, true); | ||
| d.body.removeChild(overlay); | ||
| showToast('You will not be asked again.'); | ||
| }); | ||
| } | ||
|
|
||
| function reportUpgradeEvent(oldVersion, newVersion) { | ||
| showToast('Reporting upgrade...'); | ||
|
|
||
| // Fetch fresh data from /json/info endpoint as requested | ||
| fetch('/json/info', { | ||
| method: 'get' | ||
| }) | ||
| .then(res => res.json()) | ||
| .then(infoData => { | ||
| // Map to UpgradeEventRequest structure per OpenAPI spec | ||
| // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 | ||
| const upgradeData = { | ||
| deviceId: infoData.deviceId, // Use anonymous unique device ID | ||
| version: infoData.ver || '', // Current version string | ||
| previousVersion: oldVersion || '', // Previous version from version-info.json | ||
| releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0") | ||
| chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) | ||
| ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs | ||
| isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup | ||
| bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash - not yet availeable in WLEDMM | ||
| brand: infoData.brand, // Device brand (always present) | ||
| product: infoData.product, // Product name (always present) | ||
| flashSize: infoData.flash // Flash size (always present) | ||
| }; | ||
| // Add optional fields if available | ||
| if (infoData.tpsram !== undefined) upgradeData.psramSize = Math.round(infoData.tpsram / (1024 * 1024)); // convert bytes to MB - tpsram is MM specific | ||
| // Note: partitionSizes not currently available in /json/info endpoint | ||
| // it is availeable in WLEDMM => infoData.t = total FS size in bytes | ||
|
|
||
| // Make AJAX call to postUpgradeEvent API | ||
| return fetch('https://usage.wled.me/api/usage/upgrade', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify(upgradeData) | ||
| }); | ||
|
Comment on lines
+3821
to
+3827
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add timeout and improve error handling for external API call. The fetch request to Add a timeout and better error context: // Make AJAX call to postUpgradeEvent API
- return fetch('https://usage.wled.me/api/usage/upgrade', {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
+
+ return fetch('https://usage.wled.me/api/usage/upgrade', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify(upgradeData)
+ body: JSON.stringify(upgradeData),
+ signal: controller.signal
+ }).finally(() => {
+ clearTimeout(timeoutId);
});
})
.then(res => {
if (res.ok) {
showToast('Thank you for reporting!');
updateVersionInfo(newVersion, false);
} else {
- showToast('Report failed. Please try again later.', true);
+ showToast(`Report failed (${res.status}). Please try again later.`, true);
// Do NOT update version info on failure - user will be prompted again
}
})
.catch(e => {
- console.log('Failed to report upgrade', e);
- showToast('Report failed. Please try again later.', true);
+ const isTimeout = e.name === 'AbortError';
+ console.log('Failed to report upgrade', e);
+ showToast(isTimeout ? 'Report timed out. Please try again later.' : 'Report failed. Please try again later.', true);
// Do NOT update version info on error - user will be prompted again
});
🤖 Prompt for AI Agents |
||
| }) | ||
| .then(res => { | ||
| if (res.ok) { | ||
| showToast('Thank you for reporting!'); | ||
| updateVersionInfo(newVersion, false); | ||
| } else { | ||
| showToast('Report failed. Please try again later.', true); | ||
| // Do NOT update version info on failure - user will be prompted again | ||
| } | ||
| }) | ||
| .catch(e => { | ||
| console.log('Failed to report upgrade', e); | ||
| showToast('Report failed. Please try again later.', true); | ||
| // Do NOT update version info on error - user will be prompted again | ||
| }); | ||
| } | ||
|
|
||
| function updateVersionInfo(version, neverAsk) { | ||
| const versionInfo = { | ||
| version: version, | ||
| neverAsk: neverAsk | ||
| }; | ||
|
|
||
| // Create a Blob with JSON content and use /upload endpoint | ||
| const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' }); | ||
| const formData = new FormData(); | ||
| formData.append('data', blob, 'version-info.json'); | ||
|
|
||
| fetch('/upload', { | ||
| method: 'POST', | ||
| body: formData | ||
| }) | ||
| .then(res => res.text()) | ||
| .then(data => { | ||
| console.log('Version info updated', data); | ||
| }) | ||
| .catch(e => { | ||
| console.log('Failed to update version-info.json', e); | ||
| }); | ||
| } | ||
|
|
||
| size(); | ||
| _C.style.setProperty('--n', N); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
version-info.jsonis likely never saved (/uploadfield name mismatch, nolocsupport)The version-reporting flow is well thought out overall, but there are two concrete issues that will likely break persistence and file-mode behavior:
/uploadExisting code uses
uploadFileWithText()to POST files to/upload:The new
updateVersionInfo()instead does:The server-side upload handler in WLED expects the field name
"upload"(as evidenced byuploadFileWithText); sending"data"means the firmware will not recognize or store the file. As a result:version-info.jsonwill never be created/updated.checkVersionUpgrade()will keep treating this as a first install or as a version mismatch, so users will be re-prompted indefinitely.You should either:
"upload"and ensure the filename path matches what/edit?edit=version-info.jsonexpects.loc/locipOther fetches in this file consistently use
(loc ? \http://${locip}` : '')as a prefix so the UI works both when served by the device and when opened fromfile:`. The new logic uses bare paths:fetch('/edit?edit=version-info.json', ...)fetch('/json/info', ...)fetch('/upload', ...)In file‑mode these will target the local filesystem origin instead of the WLED device, so:
checkVersionUpgrade()will fail its fetch, fall into thecatch, and attempt to POST/uploadto the wrong origin.reportUpgradeEvent()will fail to fetch/json/infoat all.If you care about file‑mode parity, you should follow the same pattern used elsewhere:
and similarly for
/json/infoand/upload.Once those two points are fixed, the flow (single per-load check, honor
neverAsk, prompt on real version changes, and build anupgradeDatapayload from/json/info) looks structurally sound.Also applies to: 3690-3871
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai i have tested the code (latest commit of this PR) on several boards, and
version-info.jsonis properly created on the device, and it is also loaded and recognized by the JS function.please re-check your analysis, I think you made some wrong assumptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@softhack007, thank you for testing and providing that feedback! I apologize for the incorrect analysis. If
version-info.jsonis being properly created and recognized on the device, then my assumptions about the/uploadendpoint were clearly wrong.I'm curious about one thing for my own learning: I noticed that the new
updateVersionInfo()function uses'data'as the FormData field name:While the existing
uploadFileWithText()function uses'upload':Does the WLED firmware
/uploadendpoint accept both field names, or does it perhaps just look at the filename regardless of the field name? Understanding this would help me avoid similar misassumptions in the future.In any case, I appreciate you taking the time to test this thoroughly—empirical evidence always beats theoretical analysis!
🧠 Learnings used