fix(web): resolve remaining valid findings from latest review pass

- custom-feeds.js: fix asset-upload contract mismatch (field name "file" ->
  "files", response read from top-level "uploaded_files" not "data.files") -
  same bug already fixed in this file on a separate branch/PR (#420), which
  this branch never received since they're independent PRs off main
- custom-feeds.js: add aria-label to the two icon-only "remove feed" buttons
- custom-feeds.js: move file-input reset into .finally() so a failed upload
  doesn't leave the input stuck holding the file, blocking retry of the
  same file
- app-shell.js: fix executePluginAction(pluginId, actionId) parameter
  order/count mismatch vs. its callers' (actionId, index, pluginId) -
  currently masked by plugins_manager.js's correct version overwriting this
  one at load time (classic vs. deferred script order), but worth fixing
  outright since it's an isolated, self-contained reassignment (not inside
  the Alpine app() object literal) and removes a latent footgun
- overview.html: align Alpine-state resolution with settings-search.js's
  two-tier getAppData() (also check appEl.__x.$data, not just _x_dataStack)

Verified already addressed by earlier passes (no change needed):
plugin_rotation_order validation, DisplayController log prefixes, togglePlugin
returning its promise for install-flow chaining, installedPlugins setter
always updating state, mobile-nav aria-label, toggleSection aria-expanded
sync, PluginOrderList bounded init retries (both display.html and
durations.html), plugin-order-list.js Array.isArray validation, batched
getImageData in the LED-dot preview renderer, app.py exception narrowing/
logging, form-submission log redaction.

Confirmed dead code, skipped (unreachable - zero template/JS callers,
verified via full-repo grep): dotToNested prototype-pollution hardening,
generateFieldHtml HTML-injection hardening, and the HTML-entity-unescape
block in JSON parsing - all three live only inside app-shell.js's two
legacy savePluginConfig implementations (one Alpine-method, one standalone),
neither of which any template or script calls. The real, live plugin-config
path is server-rendered via GET /partials/plugin-config/<id>.

Explicitly NOT reverted: the htmx:afterSwap script-execution listener. An
earlier finding batch asked to remove it as "duplicate" htmx behavior; that
was tried and reverted this session after live testing on hardware proved
it broke every partial whose Alpine x-data depends on an inline <script>
in the same partial (confirmed: WiFi tab hard-failed with "wifiSetup is not
defined"). Removing it again would reintroduce that regression.
This commit is contained in:
ChuckBuilds
2026-07-16 20:31:49 -04:00
parent 14df879e31
commit 6a93b2c89e
3 changed files with 19 additions and 7 deletions
+1 -1
View File
@@ -2775,7 +2775,7 @@
} }
}; };
window.executePluginAction = function(pluginId, actionId) { window.executePluginAction = function(actionId, actionIndex, pluginId) {
fetch(`/api/v3/plugins/action?plugin_id=${pluginId}&action_id=${actionId}`, { fetch(`/api/v3/plugins/action?plugin_id=${pluginId}&action_id=${actionId}`, {
method: 'POST' method: 'POST'
}) })
@@ -209,6 +209,7 @@
const removeButton = document.createElement('button'); const removeButton = document.createElement('button');
removeButton.type = 'button'; removeButton.type = 'button';
removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1'; removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1';
removeButton.setAttribute('aria-label', 'Remove feed');
removeButton.addEventListener('click', function() { removeButton.addEventListener('click', function() {
window.removeCustomFeedRow(this); window.removeCustomFeedRow(this);
}); });
@@ -333,6 +334,7 @@
const removeButton = document.createElement('button'); const removeButton = document.createElement('button');
removeButton.type = 'button'; removeButton.type = 'button';
removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1'; removeButton.className = 'text-red-600 hover:text-red-800 px-2 py-1';
removeButton.setAttribute('aria-label', 'Remove feed');
removeButton.addEventListener('click', function() { removeButton.addEventListener('click', function() {
window.removeCustomFeedRow(this); window.removeCustomFeedRow(this);
}); });
@@ -404,7 +406,10 @@
if (!file) return; if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); // Backend contract (api_v3.upload_plugin_asset): field must be named
// "files" (request.files.getlist('files')), and the response carries
// results in a top-level "uploaded_files" key, not nested under "data".
formData.append('files', file);
formData.append('plugin_id', pluginId); formData.append('plugin_id', pluginId);
fetch('/api/v3/plugins/assets/upload', { fetch('/api/v3/plugins/assets/upload', {
@@ -421,8 +426,8 @@
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
if (data.status === 'success' && data.data && data.data.files && data.data.files.length > 0) { if (data.status === 'success' && data.uploaded_files && data.uploaded_files.length > 0) {
const uploadedFile = data.data.files[0]; const uploadedFile = data.uploaded_files[0];
const row = document.querySelector(`#${fieldId}_tbody tr[data-index="${index}"]`); const row = document.querySelector(`#${fieldId}_tbody tr[data-index="${index}"]`);
if (row) { if (row) {
const logoCell = row.querySelector('td:nth-child(3)'); const logoCell = row.querySelector('td:nth-child(3)');
@@ -495,8 +500,6 @@
// Append container to logoCell // Append container to logoCell
logoCell.appendChild(container); logoCell.appendChild(container);
} }
// Allow re-uploading the same file
event.target.value = '';
} else { } else {
const notifyFn = window.showNotification || alert; const notifyFn = window.showNotification || alert;
notifyFn('Upload failed: ' + (data.message || 'Unknown error'), 'error'); notifyFn('Upload failed: ' + (data.message || 'Unknown error'), 'error');
@@ -506,6 +509,12 @@
console.error('Upload error:', error); console.error('Upload error:', error);
const notifyFn = window.showNotification || alert; const notifyFn = window.showNotification || alert;
notifyFn('Upload failed: ' + error.message, 'error'); notifyFn('Upload failed: ' + error.message, 'error');
})
.finally(() => {
// Reset regardless of outcome, so the same file can be re-selected
// to retry after a failure (browsers won't fire "change" again
// for an input that still holds that exact file).
event.target.value = '';
}); });
}; };
@@ -153,7 +153,10 @@
return; return;
} }
var appEl = document.querySelector('[x-data="app()"]') || document.querySelector('[x-data]'); var appEl = document.querySelector('[x-data="app()"]') || document.querySelector('[x-data]');
var data = appEl && appEl._x_dataStack && appEl._x_dataStack[0]; // Same two-tier resolution as settings-search.js's getAppData():
// _x_dataStack on current Alpine, __x.$data as an older-API fallback.
var data = appEl && ((appEl._x_dataStack && appEl._x_dataStack[0]) ||
(appEl.__x && appEl.__x.$data));
if (data) { if (data) {
data.activeTab = btn.dataset.tab; data.activeTab = btn.dataset.tab;
if ('mobileNavOpen' in data) data.mobileNavOpen = false; if ('mobileNavOpen' in data) data.mobileNavOpen = false;