Files
LEDMatrix/web_interface/static/v3/js/widgets/time-range.js
T
ChuckandClaude Opus 5.5 7e066174d9 chore: remove dead code, deprecate unused plugin APIs (over-engineering audit)
Whole-tree audit. Every symbol was checked against core, the plugin
monorepo and all eight third-party plugins in plugins.json first.

- Deprecate (removal 3.10.0) plugin-facing methods nothing calls:
  LogoDownloader bulk download, ConfigManager backup/secret wrappers,
  APIHelper extras, BackgroundDataService poll API, PluginManager /
  PluginStateManager info readers, and a few CacheManager, FontManager,
  BaseOddsManager, DynamicTeamResolver methods and PluginTestCase.
  plugin_api_usage.py learns their receiver names; DEPRECATIONS doc
  regenerated.
- Remove core-internal dead code: CacheMetrics, Vegas status/stats
  plumbing, sync "new cycle" message (followers ignore unknown types),
  unused operation types, test-only PluginCatalog readers, IPC to_dict
  and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator
  callbacks, duplicate web response helpers.
- Web UI: drop never-mounted json-file-manager.js, the example widget,
  utils/error_handler.js, four uncalled PluginAPI methods, and 29
  escapeHtml shims (call window.LEDEscape directly). Public globals,
  BaseWidget and widget names unchanged.
- Remove six one-off scripts (owner decision) and the unused markupsafe
  and pytest-mock pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:27:40 -04:00

367 lines
14 KiB
JavaScript

/**
* LEDMatrix Time Range Widget
*
* Reusable paired start/end time inputs with validation.
* Can be used by any plugin via x-widget: "time-range" in their schema.
*
* Schema example:
* {
* "quiet_hours": {
* "type": "object",
* "x-widget": "time-range",
* "properties": {
* "start_time": { "type": "string", "format": "time" },
* "end_time": { "type": "string", "format": "time" }
* },
* "x-options": {
* "allowOvernight": true, // Allow end < start (overnight schedules)
* "showDuration": false, // Show calculated duration
* "disabled": false, // Start disabled
* "startLabel": "Start", // Custom label for start time
* "endLabel": "End" // Custom label for end time
* }
* }
* }
*
* @module TimeRangeWidget
*/
(function() {
'use strict';
// Use BaseWidget utilities if available
const base = window.BaseWidget ? new window.BaseWidget('TimeRange', '1.0.0') : null;
function sanitizeId(id) {
if (base) return base.sanitizeId(id);
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
}
function triggerChange(fieldId, value) {
if (base) {
base.triggerChange(fieldId, value);
} else {
const event = new CustomEvent('widget-change', {
detail: { fieldId, value },
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
}
}
function showError(container, message) {
if (base) {
base.showError(container, message);
} else {
clearError(container);
const errorEl = document.createElement('div');
errorEl.className = 'widget-error text-sm text-red-600 mt-2';
errorEl.textContent = message;
container.appendChild(errorEl);
}
}
function clearError(container) {
if (base) {
base.clearError(container);
} else {
const errorEl = container.querySelector('.widget-error');
if (errorEl) errorEl.remove();
}
}
/**
* Parse time string to minutes since midnight
* @param {string} timeStr - Time in HH:MM format
* @returns {number} Minutes since midnight, or -1 if invalid
*/
function parseTimeToMinutes(timeStr) {
if (!timeStr || typeof timeStr !== 'string') return -1;
const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return -1;
const hours = parseInt(match[1], 10);
const minutes = parseInt(match[2], 10);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return -1;
return hours * 60 + minutes;
}
/**
* Calculate duration between two times
* @param {string} startTime - Start time HH:MM
* @param {string} endTime - End time HH:MM
* @param {boolean} allowOvernight - Whether overnight is allowed
* @returns {string} Duration string
*/
function calculateDuration(startTime, endTime, allowOvernight) {
const startMinutes = parseTimeToMinutes(startTime);
const endMinutes = parseTimeToMinutes(endTime);
if (startMinutes < 0 || endMinutes < 0) return '';
let durationMinutes;
if (endMinutes >= startMinutes) {
durationMinutes = endMinutes - startMinutes;
} else if (allowOvernight) {
durationMinutes = (24 * 60 - startMinutes) + endMinutes;
} else {
return 'Invalid range';
}
const hours = Math.floor(durationMinutes / 60);
const minutes = durationMinutes % 60;
if (hours === 0) return `${minutes}m`;
if (minutes === 0) return `${hours}h`;
return `${hours}h ${minutes}m`;
}
window.LEDMatrixWidgets.register('time-range', {
name: 'Time Range Widget',
version: '1.0.0',
/**
* Render the time range widget
* @param {HTMLElement} container - Container element
* @param {Object} config - Schema configuration
* @param {Object} value - Object with start_time and end_time
* @param {Object} options - Additional options (fieldId, pluginId)
*/
render: function(container, config, value, options) {
const fieldId = sanitizeId(options.fieldId || container.id || 'time_range');
const xOptions = config['x-options'] || config['x_options'] || {};
const allowOvernight = xOptions.allowOvernight !== false;
const showDuration = xOptions.showDuration === true;
const disabled = xOptions.disabled === true;
const startLabel = xOptions.startLabel || 'Start Time';
const endLabel = xOptions.endLabel || 'End Time';
// Normalize value
const startTime = (value && value.start_time) || '07:00';
const endTime = (value && value.end_time) || '23:00';
const disabledAttr = disabled ? 'disabled' : '';
const disabledClass = disabled ? 'bg-gray-100 cursor-not-allowed' : '';
const inputName = options.name || fieldId;
let html = `<div id="${fieldId}_widget" class="time-range-widget" data-field-id="${fieldId}" data-allow-overnight="${allowOvernight}">`;
// Hidden inputs for form submission
html += `<input type="hidden" id="${fieldId}_start_time" name="${inputName}_start_time" value="${window.LEDEscape.html(startTime)}">`;
html += `<input type="hidden" id="${fieldId}_end_time" name="${inputName}_end_time" value="${window.LEDEscape.html(endTime)}">`;
html += `<div class="grid grid-cols-1 md:grid-cols-2 gap-4">`;
// Start time input
html += `
<div class="form-group">
<label for="${fieldId}_start_input" class="block text-sm font-medium text-gray-700">${window.LEDEscape.html(startLabel)}</label>
<input type="time"
id="${fieldId}_start_input"
value="${window.LEDEscape.html(startTime)}"
${disabledAttr}
onchange="window.LEDMatrixWidgets.getHandlers('time-range').onChange('${fieldId}')"
class="form-control mt-1 ${disabledClass}">
</div>
`;
// End time input
html += `
<div class="form-group">
<label for="${fieldId}_end_input" class="block text-sm font-medium text-gray-700">${window.LEDEscape.html(endLabel)}</label>
<input type="time"
id="${fieldId}_end_input"
value="${window.LEDEscape.html(endTime)}"
${disabledAttr}
onchange="window.LEDMatrixWidgets.getHandlers('time-range').onChange('${fieldId}')"
class="form-control mt-1 ${disabledClass}">
</div>
`;
html += '</div>';
// Duration display
if (showDuration) {
const duration = calculateDuration(startTime, endTime, allowOvernight);
html += `
<div id="${fieldId}_duration" class="mt-2 text-sm text-gray-500">
Duration: <span class="font-medium">${window.LEDEscape.html(duration)}</span>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
},
/**
* Get current time range value
* @param {string} fieldId - Field ID
* @returns {Object} Object with start_time and end_time
*/
getValue: function(fieldId) {
const safeId = sanitizeId(fieldId);
const startInput = document.getElementById(`${safeId}_start_input`);
const endInput = document.getElementById(`${safeId}_end_input`);
return {
start_time: startInput ? startInput.value : '',
end_time: endInput ? endInput.value : ''
};
},
/**
* Set time range value
* @param {string} fieldId - Field ID
* @param {Object} value - Object with start_time and end_time
*/
setValue: function(fieldId, value) {
const safeId = sanitizeId(fieldId);
const startInput = document.getElementById(`${safeId}_start_input`);
const endInput = document.getElementById(`${safeId}_end_input`);
const startHidden = document.getElementById(`${safeId}_start_time`);
const endHidden = document.getElementById(`${safeId}_end_time`);
const startTime = (value && value.start_time) || '';
const endTime = (value && value.end_time) || '';
if (startInput) startInput.value = startTime;
if (endInput) endInput.value = endTime;
if (startHidden) startHidden.value = startTime;
if (endHidden) endHidden.value = endTime;
// Update duration if shown
this.handlers.updateDuration(fieldId);
},
/**
* Validate the time range
* @param {string} fieldId - Field ID
* @returns {Object} { valid: boolean, errors: Array }
*/
validate: function(fieldId) {
const safeId = sanitizeId(fieldId);
const widget = document.getElementById(`${safeId}_widget`);
const value = this.getValue(fieldId);
const errors = [];
// Check for empty values
if (!value.start_time) {
errors.push('Start time is required');
}
if (!value.end_time) {
errors.push('End time is required');
}
// Validate time format
if (value.start_time && parseTimeToMinutes(value.start_time) < 0) {
errors.push('Invalid start time format');
}
if (value.end_time && parseTimeToMinutes(value.end_time) < 0) {
errors.push('Invalid end time format');
}
// Check for valid range if overnight not allowed
if (widget && errors.length === 0) {
const allowOvernight = widget.dataset.allowOvernight === 'true';
if (!allowOvernight) {
const startMinutes = parseTimeToMinutes(value.start_time);
const endMinutes = parseTimeToMinutes(value.end_time);
if (endMinutes <= startMinutes) {
errors.push('End time must be after start time');
}
}
}
// Show/clear errors
if (widget) {
if (errors.length > 0) {
showError(widget, errors[0]);
} else {
clearError(widget);
}
}
return {
valid: errors.length === 0,
errors
};
},
/**
* Set disabled state
* @param {string} fieldId - Field ID
* @param {boolean} disabled - Whether to disable
*/
setDisabled: function(fieldId, disabled) {
const safeId = sanitizeId(fieldId);
const startInput = document.getElementById(`${safeId}_start_input`);
const endInput = document.getElementById(`${safeId}_end_input`);
if (startInput) {
startInput.disabled = disabled;
startInput.classList.toggle('bg-gray-100', disabled);
startInput.classList.toggle('cursor-not-allowed', disabled);
}
if (endInput) {
endInput.disabled = disabled;
endInput.classList.toggle('bg-gray-100', disabled);
endInput.classList.toggle('cursor-not-allowed', disabled);
}
},
handlers: {
/**
* Handle time input change
* @param {string} fieldId - Field ID
*/
onChange: function(fieldId) {
const widget = window.LEDMatrixWidgets.get('time-range');
const value = widget.getValue(fieldId);
const safeId = sanitizeId(fieldId);
// Update hidden inputs
const startHidden = document.getElementById(`${safeId}_start_time`);
const endHidden = document.getElementById(`${safeId}_end_time`);
if (startHidden) startHidden.value = value.start_time;
if (endHidden) endHidden.value = value.end_time;
// Update duration
this.updateDuration(fieldId);
// Validate
widget.validate(fieldId);
// Trigger change event
triggerChange(fieldId, value);
},
/**
* Update duration display
* @param {string} fieldId - Field ID
*/
updateDuration: function(fieldId) {
const safeId = sanitizeId(fieldId);
const durationEl = document.getElementById(`${safeId}_duration`);
if (!durationEl) return;
const widget = window.LEDMatrixWidgets.get('time-range');
const value = widget.getValue(fieldId);
const widgetEl = document.getElementById(`${safeId}_widget`);
const allowOvernight = widgetEl && widgetEl.dataset.allowOvernight === 'true';
const duration = calculateDuration(value.start_time, value.end_time, allowOvernight);
const spanEl = durationEl.querySelector('span');
if (spanEl) {
spanEl.textContent = duration;
}
}
}
});
// Expose utility functions for external use
window.LEDMatrixWidgets.get('time-range').parseTimeToMinutes = parseTimeToMinutes;
window.LEDMatrixWidgets.get('time-range').calculateDuration = calculateDuration;
})();