- Python 66.9%
- HTML 31.6%
- Shell 0.7%
- CSS 0.4%
- Dockerfile 0.3%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| compose | ||
| conductor | ||
| conductor-pi | ||
| core | ||
| deploy | ||
| docs | ||
| mealplanner | ||
| scripts | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .python-version | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Makefile | ||
| manage.py | ||
| mealplanner-spec.md | ||
| mealplanner_backup.sh | ||
| plan.md | ||
| pyproject.toml | ||
| README.md | ||
| TODO.md | ||
| uv.lock | ||
Meal Planner
A family meal planning application built with Django, HTMX, and Tailwind CSS.
Production is Docker Compose on ser, with public TLS nginx on
ballesteros proxying over Tailscale to :8082. SQLite is the database.
See docs/docker-migration-playbook.md and docs/backup-and-restore.md.
Local Development
Prerequisites
- uv (Python package manager)
- Tailwind CLI binary: Download via:
(Note: Adjust URL for non-Linux platforms)curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 && chmod +x tailwindcss-linux-x64 && mv tailwindcss-linux-x64 tailwindcss
Running the Application
-
Install dependencies:
uv sync -
Database setup:
uv run manage.py migrate uv run manage.py seed_data -
Start the development server:
uv run manage.py runserver -
Watch for CSS changes (in a separate terminal):
make css-watch
CSS Build Commands
make css: Performs a one-time minified build ofstyles.css.make css-watch: Watchesinput.cssand all templates for changes and rebuilds CSS automatically.
API Access
The Meal Planner includes a RESTful API with comprehensive endpoints for all models.
Food vocabulary & childcare integration (Phase 1)
This repo is the read-only food database for the Seven Steps Childminding
integration — see docs/childcare-integration.md.
New read endpoints (all behind IsAuthenticated; the food-service group stays
read-only):
| Endpoint | Notes |
|---|---|
GET /api/v1/allergens/ |
Open catalog, seeded with the 14 UK allergens (no other). |
GET /api/v1/component-types/ |
Card/slot types (carb, protein, fruit/veg, drink), ordered. |
GET /api/v1/options/ |
The "cards". Filters: ?component_type= (slug or public_id), ?search= (name/keywords), ?backup=1. |
GET /api/v1/catalog/version/ |
{"version", "computed_at"}; moves when any catalog row or allergen link changes. |
POST /api/v1/shopping-need/ |
Stateless aggregated ingredient need (see below). Nothing is written. |
Cross-repo references use stable keys, never integer PKs: Allergen, Option,
Recipe and Ingredient expose an immutable slug + public_id, and every
food read serializer exposes them. Recipe/Option also expose a derived
allergen_signature that changes whenever a contributing ingredient's allergens
change; structured contains/may_contain links are the only screening input,
while allergen_notes is context only.
Breaking change.
Recipe,OptionandIngredientdetail URLs are now keyed bypublic_id, not the integer pk:GET /api/v1/recipes/<public_id>/. Integer-pk detail URLs now return 404. The only consumer is Seven Steps, which reads list endpoints. Write requests still take the pk FKs.
# Catalog version (freshness check for a mirror)
curl -s http://localhost:8000/api/v1/catalog/version/ \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq '.'
# Recipe detail by stable public_id
curl -s "http://localhost:8000/api/v1/recipes/$RECIPE_PUBLIC_ID/" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq '.'
# Shopping need: scale these recipes to these portion counts (no writes)
curl -s -X POST http://localhost:8000/api/v1/shopping-need/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"occurrences":[{"recipe":"'"$RECIPE_PUBLIC_ID"'","portions":6,"serves":4}]}' | jq '.'
shopping-need scales each recipe's free-text quantities by portions / serves
("400g" ×1.5 → "600g"; a non-numeric quantity keeps its text and gains a
×1.5), aggregates with the existing aggregate_quantities(), and splits pantry
staples into their own pantry list. An unknown public_id returns 400.
Authentication
The API supports both session-based and token-based (JWT) authentication:
Session Authentication (Web Browsers)
# Login to establish session
curl -X POST http://localhost:8000/accounts/login/ \
-d "username=youruser&password=yourpass" \
--cookie-jar cookies.txt
# Use session cookie for API requests
curl http://localhost:8000/api/v1/recipes/ \
--cookie cookies.txt
Token Authentication (Mobile Apps, CLI, Third-Party Services)
Important: Always include -H "Accept: application/json" to get JSON responses instead of HTML.
# Get JWT tokens (note: Accept header is required for JSON response)
curl -s -X POST http://localhost:8000/api/v1/token/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"youruser","password":"yourpass"}'
# Response includes access and refresh tokens:
# {
# "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
# Use access token for API requests
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Accept: application/json"
# Refresh access token when expired
curl -s -X POST http://localhost:8000/api/v1/token/refresh/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'
CLI Usage Examples (Bash)
For convenient CLI usage, save the token in a variable and reuse it:
# Login and save token to variable
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/token/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"matt","password":"euShuuZog1ahfaiVa2"}' | jq -r '.access')
# Now use $TOKEN for subsequent requests
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
# List all recipes with pretty printing
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Create a new recipe
curl -s -X POST http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "My New Recipe",
"meal_type": 1,
"difficulty": 2,
"instructions": "Step 1: Do this. Step 2: Do that."
}'
# Get a specific recipe (detail URLs use the stable public_id)
curl -s "http://localhost:8000/api/v1/recipes/$RECIPE_PUBLIC_ID/" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Update a recipe
curl -s -X PUT "http://localhost:8000/api/v1/recipes/$RECIPE_PUBLIC_ID/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "Updated Recipe Name"}'
# Delete a recipe
curl -s -X DELETE "http://localhost:8000/api/v1/recipes/$RECIPE_PUBLIC_ID/" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
Week Plan Examples
# Assuming $TOKEN is set from login above
# List all week plans
curl -s http://localhost:8000/api/v1/week-plans/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Create a new week plan
curl -s -X POST http://localhost:8000/api/v1/week-plans/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"start_date": "2026-06-01"}'
# Shuffle meals for a week plan (custom action)
curl -s -X POST http://localhost:8000/api/v1/week-plans/1/shuffle/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
# Get planned meals for a week plan
curl -s http://localhost:8000/api/v1/week-plans/1/planned_meals/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
Shopping List Examples
# Assuming $TOKEN is set from login above
# List all shopping lists
curl -s http://localhost:8000/api/v1/shopping-lists/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Generate shopping list from a week plan
curl -s -X POST http://localhost:8000/api/v1/shopping-lists/1/generate/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"week_plan": 1, "store": 1}'
# Get items for a shopping list
curl -s http://localhost:8000/api/v1/shopping-lists/1/items/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Toggle check status on an item
curl -s -X POST http://localhost:8000/api/v1/shopping-list-items/1/toggle_check/ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
Filtering Examples
# Filter recipes by meal type
curl -s "http://localhost:8000/api/v1/recipes/?meal_type=1" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Filter recipes by difficulty
curl -s "http://localhost:8000/api/v1/recipes/?difficulty=2" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Search recipes by name
curl -s "http://localhost:8000/api/v1/recipes/?search=chicken" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
# Combine filters
curl -s "http://localhost:8000/api/v1/recipes/?meal_type=1&difficulty=2&search=pasta" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq '.'
Token Management
The Meal Planner API uses JWT tokens with refresh tokens for secure authentication. This means:
- Access Token: Valid for 5 minutes, used for API requests
- Refresh Token: Valid for 1 day, used to get new access tokens without re-entering credentials
- Token Rotation: Refresh tokens rotate on use for security (
ROTATE_REFRESH_TOKENS=True)
Why use refresh tokens? Instead of re-entering your username/password every 5 minutes, you use the refresh token to silently obtain new access tokens.
Token Refresh Workflow
# Step 1: Login to get both tokens
TOKEN_RESPONSE=$(curl -s -X POST http://localhost:8000/api/v1/token/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"matt","password":"euShuuZog1ahfaiVa2"}')
# Extract tokens
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access')
REFRESH_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.refresh')
# Step 2: Use access token for API calls (expires in 5 minutes)
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
# Step 3: When access token expires, refresh it WITHOUT credentials
# This returns a NEW access token and a ROTATED refresh token
REFRESH_RESPONSE=$(curl -s -X POST http://localhost:8000/api/v1/token/refresh/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"refresh\":\"$REFRESH_TOKEN\"}")
# Extract new tokens
ACCESS_TOKEN=$(echo $REFRESH_RESPONSE | jq -r '.access')
REFRESH_TOKEN=$(echo $REFRESH_RESPONSE | jq -r '.refresh') # This is the NEW refresh token
# Step 4: Continue using the new access token
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
# Step 5: Verify token validity (optional)
curl -s -X POST http://localhost:8000/api/v1/token/verify/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"token\":\"$ACCESS_TOKEN\"}"
Important Notes:
- Always use the latest refresh token - After refreshing, the old refresh token is invalidated
- Store refresh tokens securely - They can be used to get new access tokens
- Access tokens expire quickly (5 min) but refresh tokens last longer (1 day)
- No credentials needed for refresh - Just the refresh token itself
- Rate limited - Token endpoints have strict throttling (10/min for
/token/, 30/min for/token/refresh/)
Token Management Examples
# Get both access and refresh tokens
TOKEN_RESPONSE=$(curl -s -X POST http://localhost:8000/api/v1/token/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"username":"matt","password":"euShuuZog1ahfaiVa2"}')
# Extract both tokens
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access')
REFRESH_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.refresh')
# Use access token
curl -s http://localhost:8000/api/v1/recipes/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Accept: application/json"
# When access token expires, refresh it
NEW_TOKENS=$(curl -s -X POST http://localhost:8000/api/v1/token/refresh/ \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"refresh\":\"$REFRESH_TOKEN\"}")
# Update tokens from the response
ACCESS_TOKEN=$(echo $NEW_TOKENS | jq -r '.access')
REFRESH_TOKEN=$(echo $NEW_TOKENS | jq -r '.refresh')
API Documentation
- Swagger UI:
http://localhost:8000/api/v1/schema/swagger-ui/ - OpenAPI Schema:
http://localhost:8000/api/v1/schema/
API Endpoints
All endpoints are under /api/v1/ and require authentication. See API_IMPLEMENTATION_STATUS.md for complete endpoint documentation.
Quick Reference
| Resource | Endpoint | Methods |
|---|---|---|
| Meal Types | /api/v1/meal-types/ |
GET, POST, PUT, PATCH, DELETE |
| Shopping Categories | /api/v1/shopping-categories/ |
GET, POST, PUT, PATCH, DELETE |
| Stores | /api/v1/stores/ |
GET, POST, PUT, PATCH, DELETE |
| Ingredients | /api/v1/ingredients/ |
GET, POST, PUT, PATCH, DELETE |
| Recipes | /api/v1/recipes/ |
GET, POST, PUT, PATCH, DELETE |
| Week Plans | /api/v1/week-plans/ |
GET, POST, PUT, PATCH, DELETE |
| Planned Meals | /api/v1/planned-meals/ |
GET, POST, PUT, PATCH, DELETE |
| Shopping Lists | /api/v1/shopping-lists/ |
GET, POST, PUT, PATCH, DELETE |
| Shopping List Items | /api/v1/shopping-list-items/ |
GET, POST, PUT, PATCH, DELETE |
Custom Actions
| Action | Endpoint | Method |
|---|---|---|
| Shuffle week plan | /api/v1/week-plans/{id}/shuffle/ |
POST |
| Generate shopping list | /api/v1/shopping-lists/{id}/generate/ |
POST |
| Toggle pin on meal | /api/v1/planned-meals/{id}/toggle_pin/ |
POST |
| Toggle check on item | /api/v1/shopping-list-items/{id}/toggle_check/ |
POST |
| List planned meals | /api/v1/week-plans/{id}/planned_meals/ |
GET |
| List shopping items | /api/v1/shopping-lists/{id}/items/ |
GET |
Token Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/token/ |
POST | Obtain access and refresh tokens |
/api/v1/token/refresh/ |
POST | Refresh access token |
/api/v1/token/verify/ |
POST | Verify token validity |