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>
This commit is contained in:
parent
388ad4f5a0
commit
087abca062
188
CLAUDE.md
Normal file
188
CLAUDE.md
Normal file
@ -0,0 +1,188 @@
|
||||
# Claude Development Guide for ECM Control
|
||||
|
||||
This file contains important information for Claude Code when working on the ECM Control project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
ECM Control is an Espresso Coffee Machine Control System built with:
|
||||
- **Backend**: Python, FastAPI, SQLite
|
||||
- **Frontend**: HTMX, Bootstrap, Vanilla JavaScript
|
||||
- **Hardware**: Raspberry Pi GPIO, relay control, Acaia scale integration
|
||||
- **Package Manager**: UV (not pip/poetry)
|
||||
- **Database**: SQLite with schema in `src/ecm_control/database/schema.sql`
|
||||
|
||||
## Package Management - IMPORTANT
|
||||
|
||||
**Always use UV, never use pip or poetry:**
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
uv add package-name
|
||||
|
||||
# Remove dependencies
|
||||
uv remove package-name
|
||||
|
||||
# Run commands in virtual environment
|
||||
uv run python script.py
|
||||
|
||||
# Sync dependencies
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
**Web Application:**
|
||||
```bash
|
||||
uv run python run.py web --log-level DEBUG --port 8000
|
||||
```
|
||||
|
||||
**GPIO Control Service:**
|
||||
```bash
|
||||
# With actual hardware
|
||||
uv run python run.py gpio
|
||||
|
||||
# With mock hardware (for development)
|
||||
uv run python run.py gpio --mock --log-level DEBUG
|
||||
```
|
||||
|
||||
**Database Management:**
|
||||
```bash
|
||||
# Initialize database
|
||||
uv run python run.py db init
|
||||
|
||||
# Reset database (deletes all data)
|
||||
uv run python run.py db reset
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
**Testing endpoints:**
|
||||
- Web interface: http://localhost:8000
|
||||
- Debug with console logging enabled in browser
|
||||
- Server logs show detailed request/response info at DEBUG level
|
||||
|
||||
**Database queries for debugging:**
|
||||
```bash
|
||||
uv run python -c "
|
||||
import sys; sys.path.insert(0, 'src')
|
||||
from ecm_control.database import DatabaseManager
|
||||
db = DatabaseManager()
|
||||
recipes = db.execute_query('SELECT * FROM recipes')
|
||||
print(recipes)
|
||||
"
|
||||
```
|
||||
|
||||
## Code Style and Conventions
|
||||
|
||||
**File Structure:**
|
||||
- `src/ecm_control/` - Main package
|
||||
- `templates/` - Jinja2 HTML templates
|
||||
- `static/` - CSS/JS assets
|
||||
- `run.py` - Main entry point (use this instead of module execution)
|
||||
|
||||
**Logging:**
|
||||
- Use `from ecm_control.utils.logging_config import get_logger`
|
||||
- Logger name: `logger = get_logger('module_name')`
|
||||
- DEBUG level shows detailed request/exception info
|
||||
|
||||
**Database:**
|
||||
- Use `DatabaseManager` class for all DB operations
|
||||
- Schema in `src/ecm_control/database/schema.sql`
|
||||
- Models in `src/ecm_control/models/__init__.py`
|
||||
|
||||
## Git Workflow
|
||||
|
||||
**When making changes:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Description of changes
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
**Commit message format:**
|
||||
- Use conventional commits (feat:, fix:, docs:, etc.)
|
||||
- Include the Claude Code footer
|
||||
- Be descriptive about what changed and why
|
||||
|
||||
## Testing and Deployment
|
||||
|
||||
**Before committing:**
|
||||
1. Test web interface functionality
|
||||
2. Test GPIO service in mock mode
|
||||
3. Verify database operations work
|
||||
4. Check logs for errors/warnings
|
||||
|
||||
**Hardware Testing:**
|
||||
- Use `--mock` flag for development without GPIO hardware
|
||||
- Real hardware requires Raspberry Pi with GPIO pins
|
||||
- Scale integration uses mock implementation (pyacaia had build issues)
|
||||
|
||||
**Production Deployment:**
|
||||
- Copy `ecm-control.service` to `/etc/systemd/system/`
|
||||
- Update paths in service file for your installation
|
||||
- Use `systemctl` to manage the service
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
**"No module named ecm_control":**
|
||||
- Use `uv run python run.py` instead of `uv run python -m ecm_control`
|
||||
|
||||
**HTMX not updating UI:**
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify `hx-target` selectors match element IDs
|
||||
- Ensure `hx-swap="innerHTML"` is specified for content replacement
|
||||
|
||||
**Database errors:**
|
||||
- Reset database: `uv run python run.py db reset`
|
||||
- Check file permissions on `ecm_control.db`
|
||||
|
||||
**GPIO permissions (on Raspberry Pi):**
|
||||
```bash
|
||||
sudo usermod -a -G gpio $USER
|
||||
# Then log out and back in
|
||||
```
|
||||
|
||||
**Form validation errors (422):**
|
||||
- Check FastAPI endpoint parameter types
|
||||
- Handle empty strings vs None for optional fields
|
||||
- Use `str = Form("")` and convert to int/None in endpoint
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
**Web Application:**
|
||||
- FastAPI with dependency injection
|
||||
- HTMX for dynamic UI updates
|
||||
- Bootstrap for styling
|
||||
- Jinja2 templates with partials for HTMX responses
|
||||
|
||||
**GPIO Control:**
|
||||
- Async event-driven architecture
|
||||
- Mock implementations for development
|
||||
- Real-time shot monitoring with scale feedback
|
||||
- Database logging of all shot attempts
|
||||
|
||||
**Database Schema:**
|
||||
- `recipes` - extraction recipes (grams + timeout)
|
||||
- `buttons` - physical GPIO buttons mapped to recipes
|
||||
- `shots` - log of all extraction attempts
|
||||
- `settings` - configurable system parameters
|
||||
|
||||
## File Locations
|
||||
|
||||
**Key configuration files:**
|
||||
- `pyproject.toml` - Project dependencies and metadata
|
||||
- `ecm-control.service` - Systemd service configuration
|
||||
- `src/ecm_control/database/schema.sql` - Database schema
|
||||
- `src/ecm_control/web/__init__.py` - FastAPI web application
|
||||
- `src/ecm_control/gpio/__init__.py` - GPIO control logic
|
||||
|
||||
**Templates:**
|
||||
- `templates/base.html` - Base template with navigation
|
||||
- `templates/partials/` - HTMX response templates
|
||||
- `static/css/style.css` - Custom styles
|
||||
- `static/js/app.js` - Frontend JavaScript
|
||||
|
||||
Remember: This is a hardware control system, so safety and reliability are important. Always test thoroughly and handle errors gracefully.
|
||||
BIN
ecm_control.db
Normal file
BIN
ecm_control.db
Normal file
Binary file not shown.
0
logs/performance.log
Normal file
0
logs/performance.log
Normal file
@ -1,8 +1,7 @@
|
||||
[project]
|
||||
name = "ecm-control"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
description = "Espresso Coffee Machine Control System"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi>=0.115.13",
|
||||
@ -11,3 +10,13 @@ dependencies = [
|
||||
"python-multipart>=0.0.20",
|
||||
"uvicorn>=0.34.3",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ecm-control = "ecm_control.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/ecm_control"]
|
||||
|
||||
16
run.py
Executable file
16
run.py
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple runner script for ECM Control
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
# Import and run the main function
|
||||
from ecm_control.__main__ import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -24,24 +24,30 @@ def main():
|
||||
web_parser.add_argument('--host', default='0.0.0.0', help='Host to bind to')
|
||||
web_parser.add_argument('--port', default=8000, type=int, help='Port to bind to')
|
||||
web_parser.add_argument('--reload', action='store_true', help='Enable auto-reload')
|
||||
web_parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level')
|
||||
web_parser.add_argument('--log-file', help='Log file path')
|
||||
|
||||
# GPIO control command
|
||||
gpio_parser = subparsers.add_parser('gpio', help='Start GPIO control service')
|
||||
gpio_parser.add_argument('--mock', action='store_true', help='Use mock GPIO for testing')
|
||||
gpio_parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level')
|
||||
gpio_parser.add_argument('--log-file', help='Log file path')
|
||||
|
||||
# Database management commands
|
||||
db_parser = subparsers.add_parser('db', help='Database management')
|
||||
db_parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level')
|
||||
db_parser.add_argument('--log-file', help='Log file path')
|
||||
db_subparsers = db_parser.add_subparsers(dest='db_command')
|
||||
|
||||
db_subparsers.add_parser('init', help='Initialize database')
|
||||
db_subparsers.add_parser('reset', help='Reset database (WARNING: deletes all data)')
|
||||
|
||||
# Global options
|
||||
parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level')
|
||||
parser.add_argument('--log-file', help='Log file path')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging
|
||||
@ -83,11 +89,15 @@ def run_web_server(args):
|
||||
logger = get_logger('web')
|
||||
logger.info(f"Starting web server on {args.host}:{args.port}")
|
||||
|
||||
# Set uvicorn log level based on our log level
|
||||
uvicorn_log_level = "debug" if args.log_level.upper() == "DEBUG" else "info"
|
||||
|
||||
uvicorn.run(
|
||||
"ecm_control.web:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload
|
||||
reload=args.reload,
|
||||
log_level=uvicorn_log_level
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from fastapi import FastAPI, Request, Depends, HTTPException, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from ..database import DatabaseManager
|
||||
@ -13,6 +14,107 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="ECM Control", description="Espresso Coffee Machine Control System")
|
||||
|
||||
# Global exception handler for uncaught exceptions
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""Handle all uncaught exceptions with detailed logging when DEBUG is enabled"""
|
||||
|
||||
# Get current log level
|
||||
current_level = logger.getEffectiveLevel()
|
||||
|
||||
if current_level == logging.DEBUG:
|
||||
# Detailed logging for DEBUG level
|
||||
logger.error(f"Uncaught exception in {request.method} {request.url}")
|
||||
logger.error(f"Exception type: {type(exc).__name__}")
|
||||
logger.error(f"Exception message: {str(exc)}")
|
||||
logger.error(f"Request headers: {dict(request.headers)}")
|
||||
|
||||
# Log query parameters if any
|
||||
if request.query_params:
|
||||
logger.error(f"Query parameters: {dict(request.query_params)}")
|
||||
|
||||
# Log path parameters if any
|
||||
if hasattr(request, 'path_params') and request.path_params:
|
||||
logger.error(f"Path parameters: {request.path_params}")
|
||||
|
||||
# Log full stack trace
|
||||
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# Try to log request body for POST/PUT requests (be careful with sensitive data)
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
try:
|
||||
# This is a simplified approach - in production you'd want to be more careful
|
||||
logger.error("Request body logging attempted (not implemented for security)")
|
||||
except Exception as e:
|
||||
logger.error(f"Could not log request body: {e}")
|
||||
else:
|
||||
# Standard error logging for other levels
|
||||
logger.error(f"Uncaught exception in {request.method} {request.url}: {type(exc).__name__}: {str(exc)}")
|
||||
|
||||
# Return appropriate response based on request type
|
||||
if request.url.path.startswith("/api/") or "application/json" in request.headers.get("accept", ""):
|
||||
# Return JSON response for API calls
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"error": "Internal server error",
|
||||
"detail": str(exc) if current_level == logging.DEBUG else "An unexpected error occurred"
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Return HTML response for web requests
|
||||
try:
|
||||
return templates.TemplateResponse("error.html", {
|
||||
"request": request,
|
||||
"error_message": "An unexpected error occurred. Please try again.",
|
||||
"error_detail": str(exc) if current_level == logging.DEBUG else None,
|
||||
"status_code": 500
|
||||
}, status_code=500)
|
||||
except Exception:
|
||||
# Fallback if template rendering fails
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Error</title></head>
|
||||
<body>
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>An unexpected error occurred. Please try again.</p>
|
||||
{f'<pre>{str(exc)}</pre>' if current_level == logging.DEBUG else ''}
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
# Request logging middleware for DEBUG level
|
||||
@app.middleware("http")
|
||||
async def debug_logging_middleware(request: Request, call_next):
|
||||
"""Log all requests when DEBUG level is enabled"""
|
||||
|
||||
current_level = logger.getEffectiveLevel()
|
||||
|
||||
if current_level == logging.DEBUG:
|
||||
# Log incoming request
|
||||
logger.debug(f"🔵 {request.method} {request.url}")
|
||||
logger.debug(f"Headers: {dict(request.headers)}")
|
||||
|
||||
if request.query_params:
|
||||
logger.debug(f"Query params: {dict(request.query_params)}")
|
||||
|
||||
# Process the request
|
||||
try:
|
||||
response = await call_next(request)
|
||||
|
||||
if current_level == logging.DEBUG:
|
||||
logger.debug(f"🟢 {request.method} {request.url} → {response.status_code}")
|
||||
|
||||
return response
|
||||
|
||||
except Exception as exc:
|
||||
if current_level == logging.DEBUG:
|
||||
logger.debug(f"🔴 {request.method} {request.url} → Exception: {type(exc).__name__}")
|
||||
raise
|
||||
|
||||
# Setup static files and templates
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
@ -67,17 +169,32 @@ async def create_recipe(
|
||||
):
|
||||
"""Create a new recipe"""
|
||||
try:
|
||||
logger.info(f"Creating recipe: name={name}, grams_out={grams_out}, timeout_seconds={timeout_seconds}")
|
||||
|
||||
recipe_id = db.execute_insert(
|
||||
"INSERT INTO recipes (name, grams_out, timeout_seconds) VALUES (?, ?, ?)",
|
||||
(name, grams_out, timeout_seconds)
|
||||
)
|
||||
|
||||
logger.info(f"Recipe created with ID: {recipe_id}")
|
||||
|
||||
# Fetch updated recipes list
|
||||
recipes = db.execute_query("SELECT * FROM recipes ORDER BY name")
|
||||
logger.info(f"Fetched {len(recipes)} recipes for display")
|
||||
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
for recipe in recipes:
|
||||
logger.debug(f"Recipe: {recipe}")
|
||||
|
||||
return templates.TemplateResponse("partials/recipe_list.html", {
|
||||
"request": request,
|
||||
"recipes": recipes
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating recipe: {e}")
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
logger.error(f"Recipe creation failed - Name: {name}, Grams: {grams_out}, Timeout: {timeout_seconds}")
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=400, detail="Failed to create recipe")
|
||||
|
||||
@app.delete("/recipes/{recipe_id}")
|
||||
@ -112,14 +229,17 @@ async def create_button(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
gpio_pin: int = Form(...),
|
||||
recipe_id: Optional[int] = Form(None),
|
||||
recipe_id: str = Form(""),
|
||||
db: DatabaseManager = Depends(get_db)
|
||||
):
|
||||
"""Create a new button"""
|
||||
try:
|
||||
# Convert empty string to None, otherwise convert to int
|
||||
recipe_id_value = None if recipe_id == "" else int(recipe_id)
|
||||
|
||||
button_id = db.execute_insert(
|
||||
"INSERT INTO buttons (name, gpio_pin, recipe_id) VALUES (?, ?, ?)",
|
||||
(name, gpio_pin, recipe_id if recipe_id else None)
|
||||
(name, gpio_pin, recipe_id_value)
|
||||
)
|
||||
buttons = db.execute_query("""
|
||||
SELECT b.*, r.name as recipe_name
|
||||
@ -135,19 +255,25 @@ async def create_button(
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating button: {e}")
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
logger.error(f"Button creation failed - Name: {name}, GPIO: {gpio_pin}, Recipe ID: {recipe_id}")
|
||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=400, detail="Failed to create button")
|
||||
|
||||
@app.put("/buttons/{button_id}")
|
||||
async def update_button(
|
||||
button_id: int,
|
||||
recipe_id: Optional[int] = Form(None),
|
||||
recipe_id: str = Form(""),
|
||||
db: DatabaseManager = Depends(get_db)
|
||||
):
|
||||
"""Update button recipe mapping"""
|
||||
try:
|
||||
# Convert empty string to None, otherwise convert to int
|
||||
recipe_id_value = None if recipe_id == "" else int(recipe_id)
|
||||
|
||||
db.execute_update(
|
||||
"UPDATE buttons SET recipe_id = ? WHERE id = ?",
|
||||
(recipe_id if recipe_id else None, button_id)
|
||||
(recipe_id_value, button_id)
|
||||
)
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
|
||||
@ -3,19 +3,35 @@
|
||||
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');
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<h5 class="modal-title">Add New Button</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form hx-post="/buttons" hx-target="#button-list" hx-on="htmx:afterRequest: htmx.closest(this, '.modal').style.display='none'">
|
||||
<form hx-post="/buttons" hx-target="#button-list" hx-swap="innerHTML">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Button Name</label>
|
||||
|
||||
49
templates/error.html
Normal file
49
templates/error.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Error {{ status_code }} - ECM Control{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Error {{ status_code or 500 }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ error_message or "An unexpected error occurred" }}</h5>
|
||||
|
||||
{% if error_detail %}
|
||||
<div class="alert alert-warning mt-3">
|
||||
<h6>Debug Information:</h6>
|
||||
<pre class="mb-0"><code>{{ error_detail }}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="fas fa-home"></i>
|
||||
Return to Dashboard
|
||||
</a>
|
||||
<button onclick="history.back()" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6>Troubleshooting Tips:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li>• Check the server logs for more details</li>
|
||||
<li>• Verify that all required fields are filled correctly</li>
|
||||
<li>• Ensure your browser is up to date</li>
|
||||
<li>• Try refreshing the page</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -30,7 +30,7 @@
|
||||
<h5 class="modal-title">Add New Recipe</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form hx-post="/recipes" hx-target="#recipe-list" hx-on="htmx:afterRequest: htmx.closest(this, '.modal').style.display='none'">
|
||||
<form hx-post="/recipes" hx-target="#recipe-list" hx-swap="innerHTML">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Recipe Name</label>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user