fix(web): resolve Codacy findings — DOM building over innerHTML, Map callbacks, misc lint

Verified each of the 27 reported findings against current code; all fixed
except one rule class skipped with reason below.

- app-early.js: plugin tab buttons are now built with createElement/
  createTextNode instead of innerHTML template strings (icon class and name
  come from plugin manifests - semi-trusted input; the old code escaped the
  name but interpolated the icon class). Both construction sites. Also the
  forEach arrow no longer returns tab.remove()'s value.
- plugin-order-list.js: rows, empty state, and error state all built with
  DOM APIs - the file no longer contains innerHTML at all (the now-unneeded
  escapeHtml/escapeAttr helpers are removed); MODE_LABELS is a Map so the
  vegas-mode lookup can't hit prototype properties.
- notification.js: actionCallbacks is a Map (get/set/delete) instead of a
  plain object - resolves the object-injection-sink and dynamic-delete
  findings; triggerAction also type-checks the callback.
- htmx-config.js: unused catch binding dropped; var -> const in the
  afterSettle handler; the swapped-<script> re-execution reads/writes
  textContent instead of innerHTML; the diagnostic form payload uses a
  null-prototype object so a field named __proto__ can't pollute.
- custom-feeds-helpers.js DELETED (with its script tag): all three of its
  functions (addCustomFeedRow, removeCustomFeedRow,
  handleCustomFeedLogoUpload) are shadowed by the deferred
  widgets/custom-feeds.js window assignments, which always win at call time
  - the copies were dead even when they lived inline in base.html. This
  also resolves the unused-function and unused-variable findings there.

Skipped: 4x "Non-serializable expression must be wrapped with $(...)" in
app-early.js - that rule targets code crossing a browser-automation
serialization boundary (e.g. page.evaluate); these are ordinary arrow
functions in plain browser code with no such boundary.

Validation: all 40 web tests pass (incl. the static-asset reference audit,
which confirms no template still points at the deleted file); Jinja parse OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 13:11:17 -04:00
co-authored by Claude Sonnet 5
parent e1e26e8eab
commit e00d10ce60
6 changed files with 87 additions and 326 deletions
@@ -59,8 +59,9 @@
// Track active notifications
let activeNotifications = [];
// onAction callbacks for notifications with an inline action button,
// keyed by notification id (cleaned up on dismiss).
const actionCallbacks = {};
// keyed by notification id (cleaned up on dismiss). A Map rather than a
// plain object so ids can never collide with prototype properties.
const actionCallbacks = new Map();
let notificationCounter = 0;
/**
@@ -116,7 +117,7 @@
// Remove from tracking array
activeNotifications = activeNotifications.filter(id => id !== notificationId);
delete actionCallbacks[notificationId];
actionCallbacks.delete(notificationId);
}
/**
@@ -166,7 +167,7 @@
// The callback is stored by id and invoked via triggerAction, which
// also dismisses the notification.
if (options.actionLabel && typeof options.onAction === 'function') {
actionCallbacks[notificationId] = options.onAction;
actionCallbacks.set(notificationId, options.onAction);
html += `
<button type="button"
onclick="window.LEDMatrixWidgets.get('notification').triggerAction('${notificationId}')"
@@ -251,9 +252,9 @@
* @param {string} notificationId - Notification ID whose action to run
*/
triggerAction: function(notificationId) {
const cb = actionCallbacks[notificationId];
const cb = actionCallbacks.get(notificationId);
removeNotification(notificationId);
if (cb) cb();
if (typeof cb === 'function') cb();
},
/**