Marc Boivin 087abca062 fix: Resolve recipe/button form submission and add comprehensive debug logging
- 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>
2025-06-20 06:27:47 -04:00

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();
}
});