- Fix form parameter handling for recipe_id (empty string vs None) - Remove interfering hx-on attributes from HTMX forms - Add explicit hx-swap="innerHTML" for proper content replacement - Implement global exception handler with detailed DEBUG logging - Add request middleware for comprehensive request/response logging - Enhanced modal closing logic with fallbacks - Add debug logging to recipe/button creation endpoints - Create error template for better error display - Update CLI to support --log-level per subcommand - Add CLAUDE.md with development guidelines and conventions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
5.6 KiB
JavaScript
169 lines
5.6 KiB
JavaScript
// ECM Control Frontend JavaScript
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-hide Bootstrap modals after successful HTMX requests
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
console.log('HTMX Request completed:', event.detail);
|
|
|
|
if (event.detail.successful) {
|
|
console.log('HTMX Request was successful');
|
|
|
|
// Find any open modals and hide them
|
|
const openModals = document.querySelectorAll('.modal.show');
|
|
console.log('Found open modals:', openModals.length);
|
|
|
|
openModals.forEach(modal => {
|
|
console.log('Closing modal:', modal.id);
|
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
} else {
|
|
// Fallback: hide manually
|
|
modal.style.display = 'none';
|
|
modal.classList.remove('show');
|
|
const backdrop = document.querySelector('.modal-backdrop');
|
|
if (backdrop) {
|
|
backdrop.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Show success notification
|
|
showNotification('Operation completed successfully', 'success');
|
|
} else {
|
|
console.log('HTMX Request failed:', event.detail);
|
|
// Show error notification
|
|
showNotification('Operation failed. Please try again.', 'error');
|
|
}
|
|
});
|
|
|
|
// Clear form fields when modal is hidden
|
|
document.querySelectorAll('.modal').forEach(modal => {
|
|
modal.addEventListener('hidden.bs.modal', function() {
|
|
const form = this.querySelector('form');
|
|
if (form) {
|
|
form.reset();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Show notification function
|
|
function showNotification(message, type = 'info') {
|
|
const alertClass = type === 'success' ? 'alert-success' :
|
|
type === 'error' ? 'alert-danger' : 'alert-info';
|
|
|
|
const notification = document.createElement('div');
|
|
notification.className = `alert ${alertClass} alert-dismissible fade show position-fixed`;
|
|
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
notification.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
// Confirm deletion dialogs
|
|
document.body.addEventListener('click', function(event) {
|
|
const deleteBtn = event.target.closest('[hx-delete]');
|
|
if (deleteBtn && deleteBtn.hasAttribute('hx-confirm')) {
|
|
const confirmMessage = deleteBtn.getAttribute('hx-confirm');
|
|
if (!confirm(confirmMessage)) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Auto-refresh dashboard every 30 seconds
|
|
if (window.location.pathname === '/') {
|
|
setInterval(() => {
|
|
// Only refresh if page is visible
|
|
if (!document.hidden) {
|
|
window.location.reload();
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
// Form validation helpers
|
|
function validateRecipeForm(form) {
|
|
const name = form.querySelector('[name="name"]').value.trim();
|
|
const gramsOut = parseFloat(form.querySelector('[name="grams_out"]').value);
|
|
const timeoutSeconds = parseInt(form.querySelector('[name="timeout_seconds"]').value);
|
|
|
|
if (!name) {
|
|
showNotification('Recipe name is required', 'error');
|
|
return false;
|
|
}
|
|
|
|
if (gramsOut <= 0 || gramsOut > 100) {
|
|
showNotification('Grams out must be between 0 and 100', 'error');
|
|
return false;
|
|
}
|
|
|
|
if (timeoutSeconds <= 0 || timeoutSeconds > 300) {
|
|
showNotification('Timeout must be between 1 and 300 seconds', 'error');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function validateButtonForm(form) {
|
|
const name = form.querySelector('[name="name"]').value.trim();
|
|
const gpioPin = parseInt(form.querySelector('[name="gpio_pin"]').value);
|
|
|
|
if (!name) {
|
|
showNotification('Button name is required', 'error');
|
|
return false;
|
|
}
|
|
|
|
if (gpioPin < 0 || gpioPin > 40) {
|
|
showNotification('GPIO pin must be between 0 and 40', 'error');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Add form validation to modals
|
|
document.addEventListener('submit', function(event) {
|
|
const form = event.target;
|
|
|
|
if (form.matches('#addRecipeModal form')) {
|
|
if (!validateRecipeForm(form)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
if (form.matches('#addButtonModal form')) {
|
|
if (!validateButtonForm(form)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(event) {
|
|
// Ctrl/Cmd + N to add new recipe on recipes page
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'n' && window.location.pathname === '/recipes') {
|
|
event.preventDefault();
|
|
const modal = new bootstrap.Modal(document.getElementById('addRecipeModal'));
|
|
modal.show();
|
|
}
|
|
|
|
// Ctrl/Cmd + B to add new button on buttons page
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'b' && window.location.pathname === '/buttons') {
|
|
event.preventDefault();
|
|
const modal = new bootstrap.Modal(document.getElementById('addButtonModal'));
|
|
modal.show();
|
|
}
|
|
}); |