mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
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:
co-authored by
Claude Sonnet 5
parent
e1e26e8eab
commit
e00d10ce60
@@ -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();
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,21 +21,11 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text == null ? '' : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
const MODE_LABELS = {
|
||||
'scroll': { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' },
|
||||
'fixed': { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' },
|
||||
'static': { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }
|
||||
};
|
||||
const MODE_LABELS = new Map([
|
||||
['scroll', { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' }],
|
||||
['fixed', { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' }],
|
||||
['static', { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }]
|
||||
]);
|
||||
|
||||
function init(options) {
|
||||
const container = document.getElementById(options.containerId);
|
||||
@@ -109,7 +99,11 @@
|
||||
const allPlugins = (data.data && data.data.plugins) || data.plugins || [];
|
||||
const plugins = allPlugins.filter(p => p.enabled);
|
||||
if (plugins.length === 0) {
|
||||
container.innerHTML = '<p class="text-sm text-gray-500 italic">No enabled plugins</p>';
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'text-sm text-gray-500 italic';
|
||||
empty.textContent = 'No enabled plugins';
|
||||
container.textContent = '';
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,40 +126,55 @@
|
||||
if (!orderedPlugins.find(p => p.id === plugin.id)) orderedPlugins.push(plugin);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
// Rows are built with DOM APIs rather than innerHTML — plugin
|
||||
// ids/names come from installed manifests (semi-trusted).
|
||||
container.textContent = '';
|
||||
orderedPlugins.forEach(plugin => {
|
||||
const safePluginId = escapeAttr(plugin.id);
|
||||
const safePluginName = escapeHtml(plugin.name || plugin.id);
|
||||
let rowInner;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center p-2 bg-gray-50 rounded border border-gray-200 cursor-move plugin-order-item';
|
||||
row.dataset.pluginId = plugin.id;
|
||||
row.draggable = true;
|
||||
|
||||
const grip = document.createElement('i');
|
||||
grip.className = 'fas fa-grip-vertical text-gray-400 mr-3';
|
||||
row.appendChild(grip);
|
||||
|
||||
if (excludedInput) {
|
||||
const isExcluded = excluded.includes(plugin.id);
|
||||
rowInner = `
|
||||
<label class="flex items-center flex-1">
|
||||
<input type="checkbox"
|
||||
class="plugin-order-include h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
|
||||
${!isExcluded ? 'checked' : ''}>
|
||||
<span class="text-sm font-medium text-gray-700">${safePluginName}</span>
|
||||
</label>`;
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center flex-1';
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'plugin-order-include h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2';
|
||||
checkbox.checked = !isExcluded;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-sm font-medium text-gray-700';
|
||||
name.textContent = plugin.name || plugin.id;
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(name);
|
||||
row.appendChild(label);
|
||||
} else {
|
||||
rowInner = `<span class="text-sm font-medium text-gray-700 flex-1">${safePluginName}</span>`;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'text-sm font-medium text-gray-700 flex-1';
|
||||
name.textContent = plugin.name || plugin.id;
|
||||
row.appendChild(name);
|
||||
}
|
||||
let badge = '';
|
||||
|
||||
if (options.showVegasModeBadge) {
|
||||
const vegasMode = plugin.vegas_mode || plugin.vegas_content_type || 'fixed';
|
||||
const modeInfo = MODE_LABELS[vegasMode] || MODE_LABELS['fixed'];
|
||||
badge = `
|
||||
<span class="text-xs ${modeInfo.color} ml-2" title="Vegas display mode: ${modeInfo.label}">
|
||||
<i class="fas ${modeInfo.icon} mr-1"></i>${modeInfo.label}
|
||||
</span>`;
|
||||
const modeInfo = MODE_LABELS.get(vegasMode) || MODE_LABELS.get('fixed');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `text-xs ${modeInfo.color} ml-2`;
|
||||
badge.title = `Vegas display mode: ${modeInfo.label}`;
|
||||
const badgeIcon = document.createElement('i');
|
||||
badgeIcon.className = `fas ${modeInfo.icon} mr-1`;
|
||||
badge.appendChild(badgeIcon);
|
||||
badge.appendChild(document.createTextNode(modeInfo.label));
|
||||
row.appendChild(badge);
|
||||
}
|
||||
html += `
|
||||
<div class="flex items-center p-2 bg-gray-50 rounded border border-gray-200 cursor-move plugin-order-item"
|
||||
data-plugin-id="${safePluginId}" draggable="true">
|
||||
<i class="fas fa-grip-vertical text-gray-400 mr-3"></i>
|
||||
${rowInner}${badge}
|
||||
</div>`;
|
||||
|
||||
container.appendChild(row);
|
||||
});
|
||||
container.innerHTML = html;
|
||||
|
||||
setupDragAndDrop();
|
||||
container.querySelectorAll('.plugin-order-include').forEach(checkbox => {
|
||||
@@ -175,7 +184,11 @@
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching plugins:', error);
|
||||
container.innerHTML = '<p class="text-sm text-red-500">Error loading plugins</p>';
|
||||
const err = document.createElement('p');
|
||||
err.className = 'text-sm text-red-500';
|
||||
err.textContent = 'Error loading plugins';
|
||||
container.textContent = '';
|
||||
container.appendChild(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user