mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
fix(web): render file-upload drop zone for string-type config fields (#271)
* feat: add March Madness plugin and tournament round logos New dedicated March Madness plugin with scrolling tournament ticker: - Fetches NCAA tournament data from ESPN scoreboard API - Shows seeded matchups with team logos, live scores, and round separators - Highlights upsets (higher seed beating lower seed) in gold - Auto-enables during tournament window (March 10 - April 10) - Configurable for NCAAM and NCAAW tournaments - Vegas mode support via get_vegas_content() Tournament round logo assets: - MARCH_MADNESS.png, ROUND_64.png, ROUND_32.png - SWEET_16.png, ELITE_8.png, FINAL_4.png, CHAMPIONSHIP.png Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(store): prevent bulk-update from stalling on bundled/in-repo plugins Three related bugs caused the bulk plugin update to stall at 3/19: 1. Bundled plugins (e.g. starlark-apps, shipped with LEDMatrix rather than the plugin registry) had no metadata file, so update_plugin() returned False → API returned 500 → frontend queue halted. Fix: check for .plugin_metadata.json with install_type=bundled and return True immediately (these plugins update with LEDMatrix itself). 2. git config --get remote.origin.url (without --local) walked up the directory tree and found the parent LEDMatrix repo's remote URL for plugins that live inside plugin-repos/. This caused the store manager to attempt a 60-second git clone of the wrong repo for every update. Fix: use --local to scope the lookup to the plugin directory only. 3. hello-world manifest.json had a trailing comma causing JSON parse errors on every plugin discovery cycle (fixed on devpi directly). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address PR #263 code review findings - Replace self.is_enabled with BasePlugin.self.enabled in update(), display(), and supports_dynamic_duration() so runtime toggles work - Support quarter-based period labels for NCAAW (Q1..Q4 vs H1..H2), detected via league key or status_detail content - Use live refresh interval (60s) for cache max_age during live games instead of hardcoded 300s - Narrow broad except in _load_round_logos to (OSError, ValueError) with a fallback except Exception using logger.exception for traces - Remove unused `situation` local variable from _parse_event() - Add numpy>=1.24.0 to requirements.txt (imported but was missing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): render file-upload drop zone for string-type config fields String fields with x-widget: "file-upload" were falling through to a plain text input because the template only handled the array case. Adds a dedicated drop zone branch for string fields and corresponding handleSingleFileSelect/handleSingleFileUpload JS handlers that POST to the x-upload-config endpoint. Fixes credentials.json upload for the calendar plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address PR #271 code review findings Inline fixes: - manager.py: swap min_duration/max_duration if misconfigured, log warning - manager.py: call session.close() and null session in cleanup() to prevent socket leaks on constrained hardware - manager.py: remove blocking network I/O from display(); update() is the sole fetch path (already uses 60s live-game interval) - manager.py: guard scroll_helper None before create_scrolling_image() in _create_ticker_image() to prevent crash when ScrollHelper is unavailable - store_manager.py: replace bare "except Exception: pass" with debug log including plugin_id and path when reading .plugin_metadata.json - file-upload.js: add endpoint guard (error if uploadEndpoint is falsy), client-side extension validation from data-allowed-extensions, and response.ok check before response.json() in handleSingleFileUpload - plugin_config.html: add data-allowed-extensions attribute to single-file input so JS handler can read the allowed extensions list Nitpick fixes: - manager.py: use logger.exception() (includes traceback) instead of logger.error() for league fetch errors - manager.py: remove redundant "{e}" from logger.exception() calls for round logo and March Madness logo load errors Not fixed (by design): - manifest.json repo naming: monorepo pattern is correct per project docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(march-madness): address second round of PR #271 code review findings Inline fixes: - requirements.txt: bump Pillow to >=9.1.0 (required for Image.Resampling.LANCZOS) - file-upload.js: replace all statusDiv.innerHTML assignments with safe DOM creation (textContent + createElement) to prevent XSS from untrusted strings - plugin_config.html: add role="button", tabindex="0", aria-label, onkeydown (Enter/Space) to drop zone for keyboard accessibility; add aria-live="polite" to status div for screen-reader announcements - file-upload.js: tighten handleFileDrop endpoint check to non-empty string (dataset.uploadEndpoint.trim() !== '') so an empty attribute falls back to the multi-file handler Nitpick fixes: - manager.py: remove redundant cached_image/cached_array reassignments after create_scrolling_image() which already sets them internally - manager.py: narrow bare except in _get_team_logo to (FileNotFoundError, OSError, ValueError) for expected I/O errors; log unexpected exceptions - store_manager.py: narrow except to (OSError, ValueError) when reading .plugin_metadata.json so unrelated exceptions propagate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,12 @@
|
||||
window.handleFileDrop = function(event, fieldId) {
|
||||
event.preventDefault();
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
if (files.length === 0) return;
|
||||
// Route to single-file handler if this is a string file-upload widget
|
||||
const fileInput = document.getElementById(`${fieldId}_file_input`);
|
||||
if (fileInput && fileInput.dataset.uploadEndpoint && fileInput.dataset.uploadEndpoint.trim() !== '') {
|
||||
window.handleSingleFileUpload(fieldId, files[0]);
|
||||
} else {
|
||||
window.handleFiles(fieldId, Array.from(files));
|
||||
}
|
||||
};
|
||||
@@ -88,6 +93,118 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle single-file select for string file-upload widgets (e.g. credentials.json)
|
||||
* @param {Event} event - Change event
|
||||
* @param {string} fieldId - Field ID
|
||||
*/
|
||||
window.handleSingleFileSelect = function(event, fieldId) {
|
||||
const files = event.target.files;
|
||||
if (files.length > 0) {
|
||||
window.handleSingleFileUpload(fieldId, files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a single file for string file-upload widgets
|
||||
* Reads upload config from data attributes on the file input element.
|
||||
* @param {string} fieldId - Field ID
|
||||
* @param {File} file - File to upload
|
||||
*/
|
||||
window.handleSingleFileUpload = async function(fieldId, file) {
|
||||
const fileInput = document.getElementById(`${fieldId}_file_input`);
|
||||
if (!fileInput) return;
|
||||
|
||||
const uploadEndpoint = fileInput.dataset.uploadEndpoint;
|
||||
const targetFilename = fileInput.dataset.targetFilename || 'file.json';
|
||||
const maxSizeMB = parseFloat(fileInput.dataset.maxSizeMb || '1');
|
||||
const allowedExtensions = (fileInput.dataset.allowedExtensions || '.json')
|
||||
.split(',').map(e => e.trim().toLowerCase());
|
||||
|
||||
const statusDiv = document.getElementById(`${fieldId}_upload_status`);
|
||||
const notifyFn = window.showNotification || console.log;
|
||||
|
||||
// Guard: endpoint must be configured
|
||||
if (!uploadEndpoint) {
|
||||
notifyFn('No upload endpoint configured for this field', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate extension
|
||||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||||
if (!allowedExtensions.includes(fileExt)) {
|
||||
notifyFn(`File must be one of: ${allowedExtensions.join(', ')}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate size
|
||||
if (file.size > maxSizeMB * 1024 * 1024) {
|
||||
notifyFn(`File exceeds ${maxSizeMB}MB limit`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusDiv) {
|
||||
statusDiv.className = 'mt-2 text-xs text-gray-500';
|
||||
statusDiv.textContent = '';
|
||||
const spinner = document.createElement('i');
|
||||
spinner.className = 'fas fa-spinner fa-spin mr-1';
|
||||
statusDiv.appendChild(spinner);
|
||||
statusDiv.appendChild(document.createTextNode('Uploading...'));
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadEndpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`Server error ${response.status}: ${body}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
if (statusDiv) {
|
||||
statusDiv.className = 'mt-2 text-xs text-green-600';
|
||||
statusDiv.textContent = '';
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'fas fa-check-circle mr-1';
|
||||
statusDiv.appendChild(icon);
|
||||
statusDiv.appendChild(document.createTextNode(`Uploaded: ${targetFilename}`));
|
||||
}
|
||||
// Update hidden input with the target filename
|
||||
const hiddenInput = document.getElementById(fieldId);
|
||||
if (hiddenInput) hiddenInput.value = targetFilename;
|
||||
notifyFn(`${targetFilename} uploaded successfully`, 'success');
|
||||
} else {
|
||||
if (statusDiv) {
|
||||
statusDiv.className = 'mt-2 text-xs text-red-600';
|
||||
statusDiv.textContent = '';
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'fas fa-exclamation-circle mr-1';
|
||||
statusDiv.appendChild(icon);
|
||||
statusDiv.appendChild(document.createTextNode(`Upload failed: ${data.message}`));
|
||||
}
|
||||
notifyFn(`Upload failed: ${data.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (statusDiv) {
|
||||
statusDiv.className = 'mt-2 text-xs text-red-600';
|
||||
statusDiv.textContent = '';
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'fas fa-exclamation-circle mr-1';
|
||||
statusDiv.appendChild(icon);
|
||||
statusDiv.appendChild(document.createTextNode(`Upload error: ${error.message}`));
|
||||
}
|
||||
notifyFn(`Upload error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle multiple files upload
|
||||
* @param {string} fieldId - Field ID
|
||||
|
||||
Reference in New Issue
Block a user