No description
  • Python 66.9%
  • HTML 31.6%
  • Shell 0.7%
  • CSS 0.4%
  • Dockerfile 0.3%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-21 16:50:17 +01:00
.claude initial 2025-12-29 15:21:32 +00:00
compose Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
conductor conductor(setup): Add conductor setup files and initial track 2026-01-12 08:46:37 +00:00
conductor-pi Archive the move shopping list item conductor files 2026-01-25 17:22:16 +00:00
core Updates admin title and fixes add recipe bug 2026-09-21 16:50:17 +01:00
deploy Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
docs Flags the service-account step sevensteps depends on 2026-09-20 17:19:55 +01:00
mealplanner Updates admin title and fixes add recipe bug 2026-09-21 16:50:17 +01:00
scripts chore: Ignore generated styles.css and add a deployment step to force discard local changes to it. 2026-01-05 16:57:43 +00:00
templates feat: Enhance Shopping List Mobile UX and Layout 2026-02-01 21:20:50 +00:00
.dockerignore Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
.env.example Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
.gitignore Adds Phase 1 food vocabulary, structured allergens and signatures 2026-09-20 16:15:19 +01:00
.python-version initial 2025-12-29 15:21:32 +00:00
AGENTS.md Flags the service-account step sevensteps depends on 2026-09-20 17:19:55 +01:00
docker-compose.yml Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
Dockerfile Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
Makefile Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
manage.py initial 2025-12-29 15:21:32 +00:00
mealplanner-spec.md initial 2025-12-29 15:21:32 +00:00
mealplanner_backup.sh Add Docker Compose production stack for ser 2026-09-19 15:28:25 +01:00
plan.md chore: complete shopping list clear refinement 2026-01-25 14:35:44 +00:00
pyproject.toml API in place - not fully tested 2026-05-31 20:57:03 +01:00
README.md Adds Phase 1 food vocabulary, structured allergens and signatures 2026-09-20 16:15:19 +01:00
TODO.md Nearly completed the toast work 2026-01-26 09:36:15 +00:00
uv.lock API in place - not fully tested 2026-05-31 20:57:03 +01:00

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:
    curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 && chmod +x tailwindcss-linux-x64 && mv tailwindcss-linux-x64 tailwindcss
    
    (Note: Adjust URL for non-Linux platforms)

Running the Application

  1. Install dependencies:

    uv sync
    
  2. Database setup:

    uv run manage.py migrate
    uv run manage.py seed_data
    
  3. Start the development server:

    uv run manage.py runserver
    
  4. Watch for CSS changes (in a separate terminal):

    make css-watch
    

CSS Build Commands

  • make css: Performs a one-time minified build of styles.css.
  • make css-watch: Watches input.css and 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, Option and Ingredient detail URLs are now keyed by public_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:

  1. Always use the latest refresh token - After refreshing, the old refresh token is invalidated
  2. Store refresh tokens securely - They can be used to get new access tokens
  3. Access tokens expire quickly (5 min) but refresh tokens last longer (1 day)
  4. No credentials needed for refresh - Just the refresh token itself
  5. 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