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]
|
[project]
|
||||||
name = "ecm-control"
|
name = "ecm-control"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Espresso Coffee Machine Control System"
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.115.13",
|
"fastapi>=0.115.13",
|
||||||
@ -11,3 +10,13 @@ dependencies = [
|
|||||||
"python-multipart>=0.0.20",
|
"python-multipart>=0.0.20",
|
||||||
"uvicorn>=0.34.3",
|
"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('--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('--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('--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 control command
|
||||||
gpio_parser = subparsers.add_parser('gpio', help='Start GPIO control service')
|
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('--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
|
# Database management commands
|
||||||
db_parser = subparsers.add_parser('db', help='Database management')
|
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 = db_parser.add_subparsers(dest='db_command')
|
||||||
|
|
||||||
db_subparsers.add_parser('init', help='Initialize database')
|
db_subparsers.add_parser('init', help='Initialize database')
|
||||||
db_subparsers.add_parser('reset', help='Reset database (WARNING: deletes all data)')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
@ -83,11 +89,15 @@ def run_web_server(args):
|
|||||||
logger = get_logger('web')
|
logger = get_logger('web')
|
||||||
logger.info(f"Starting web server on {args.host}:{args.port}")
|
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(
|
uvicorn.run(
|
||||||
"ecm_control.web:app",
|
"ecm_control.web:app",
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
reload=args.reload
|
reload=args.reload,
|
||||||
|
log_level=uvicorn_log_level
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from fastapi import FastAPI, Request, Depends, HTTPException, Form
|
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.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..database import DatabaseManager
|
from ..database import DatabaseManager
|
||||||
@ -13,6 +14,107 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
app = FastAPI(title="ECM Control", description="Espresso Coffee Machine Control System")
|
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
|
# Setup static files and templates
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
@ -67,17 +169,32 @@ async def create_recipe(
|
|||||||
):
|
):
|
||||||
"""Create a new recipe"""
|
"""Create a new recipe"""
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Creating recipe: name={name}, grams_out={grams_out}, timeout_seconds={timeout_seconds}")
|
||||||
|
|
||||||
recipe_id = db.execute_insert(
|
recipe_id = db.execute_insert(
|
||||||
"INSERT INTO recipes (name, grams_out, timeout_seconds) VALUES (?, ?, ?)",
|
"INSERT INTO recipes (name, grams_out, timeout_seconds) VALUES (?, ?, ?)",
|
||||||
(name, grams_out, timeout_seconds)
|
(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")
|
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", {
|
return templates.TemplateResponse("partials/recipe_list.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"recipes": recipes
|
"recipes": recipes
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating recipe: {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")
|
raise HTTPException(status_code=400, detail="Failed to create recipe")
|
||||||
|
|
||||||
@app.delete("/recipes/{recipe_id}")
|
@app.delete("/recipes/{recipe_id}")
|
||||||
@ -112,14 +229,17 @@ async def create_button(
|
|||||||
request: Request,
|
request: Request,
|
||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
gpio_pin: int = Form(...),
|
gpio_pin: int = Form(...),
|
||||||
recipe_id: Optional[int] = Form(None),
|
recipe_id: str = Form(""),
|
||||||
db: DatabaseManager = Depends(get_db)
|
db: DatabaseManager = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Create a new button"""
|
"""Create a new button"""
|
||||||
try:
|
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(
|
button_id = db.execute_insert(
|
||||||
"INSERT INTO buttons (name, gpio_pin, recipe_id) VALUES (?, ?, ?)",
|
"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("""
|
buttons = db.execute_query("""
|
||||||
SELECT b.*, r.name as recipe_name
|
SELECT b.*, r.name as recipe_name
|
||||||
@ -135,19 +255,25 @@ async def create_button(
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating button: {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")
|
raise HTTPException(status_code=400, detail="Failed to create button")
|
||||||
|
|
||||||
@app.put("/buttons/{button_id}")
|
@app.put("/buttons/{button_id}")
|
||||||
async def update_button(
|
async def update_button(
|
||||||
button_id: int,
|
button_id: int,
|
||||||
recipe_id: Optional[int] = Form(None),
|
recipe_id: str = Form(""),
|
||||||
db: DatabaseManager = Depends(get_db)
|
db: DatabaseManager = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update button recipe mapping"""
|
"""Update button recipe mapping"""
|
||||||
try:
|
try:
|
||||||
|
# Convert empty string to None, otherwise convert to int
|
||||||
|
recipe_id_value = None if recipe_id == "" else int(recipe_id)
|
||||||
|
|
||||||
db.execute_update(
|
db.execute_update(
|
||||||
"UPDATE buttons SET recipe_id = ? WHERE id = ?",
|
"UPDATE buttons SET recipe_id = ? WHERE id = ?",
|
||||||
(recipe_id if recipe_id else None, button_id)
|
(recipe_id_value, button_id)
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -3,19 +3,35 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Auto-hide Bootstrap modals after successful HTMX requests
|
// Auto-hide Bootstrap modals after successful HTMX requests
|
||||||
document.body.addEventListener('htmx:afterRequest', function(event) {
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
console.log('HTMX Request completed:', event.detail);
|
||||||
|
|
||||||
if (event.detail.successful) {
|
if (event.detail.successful) {
|
||||||
|
console.log('HTMX Request was successful');
|
||||||
|
|
||||||
// Find any open modals and hide them
|
// Find any open modals and hide them
|
||||||
const openModals = document.querySelectorAll('.modal.show');
|
const openModals = document.querySelectorAll('.modal.show');
|
||||||
|
console.log('Found open modals:', openModals.length);
|
||||||
|
|
||||||
openModals.forEach(modal => {
|
openModals.forEach(modal => {
|
||||||
|
console.log('Closing modal:', modal.id);
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||||
if (modalInstance) {
|
if (modalInstance) {
|
||||||
modalInstance.hide();
|
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
|
// Show success notification
|
||||||
showNotification('Operation completed successfully', 'success');
|
showNotification('Operation completed successfully', 'success');
|
||||||
} else {
|
} else {
|
||||||
|
console.log('HTMX Request failed:', event.detail);
|
||||||
// Show error notification
|
// Show error notification
|
||||||
showNotification('Operation failed. Please try again.', 'error');
|
showNotification('Operation failed. Please try again.', 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
<h5 class="modal-title">Add New Button</h5>
|
<h5 class="modal-title">Add New Button</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</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="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Button Name</label>
|
<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>
|
<h5 class="modal-title">Add New Recipe</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</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="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Recipe Name</label>
|
<label for="name" class="form-label">Recipe Name</label>
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -62,7 +62,7 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ecm-control"
|
name = "ecm-control"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "gpiozero" },
|
{ name = "gpiozero" },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user