Meal Tracker
A lightweight Flask web application for tracking household meal attendance. Members respond yes or no to lunch and dinner each day, providing an at-a-glance dashboard of who will be eating at home.
Features
- User accounts — register, log in, log out with password hashing (Werkzeug)
- Households — multi-tenant; users join an existing household or create a new one on registration
- Daily meal tracking — lunch and dinner periods auto-created for each day
- Dashboard — see all household members and their responses for a given date
- History — browse past days; responses to past dates are read-only
- Admin controls — household admin can remove members or delete the entire household
- Self-service — users can delete their own account (password confirmation required)
- SQLite — zero-dependency storage, database file persisted via Docker volume
Technology Stack
| Layer |
Choice |
| Runtime |
Python 3.13 |
| Framework |
Flask 3.x |
| Database |
SQLite (via Python sqlite3) |
| Passwords |
Werkzeug generate_password_hash / check_password_hash |
| Frontend |
Jinja2 templates, vanilla CSS |
| Deployment |
Docker + docker-compose, Caddy reverse proxy network |
Project Structure
Getting Started
Prerequisites
- Docker and docker-compose
- A Caddy reverse proxy network named
caddy (already present on CozyTren infrastructure)
Quick Start
The app will be available at http://meal-tracker:5000 (reachable through Caddy if configured).
Auto-Deploy
The watch-for-updates.sh script polls the Gitea repository every 20 seconds and automatically rebuilds and redeploys the containers on new commits:
Configuration
| Environment Variable |
Default |
Description |
SECRET_KEY |
change-me-in-production |
Flask session signing secret |
DB_PATH |
/data/meals.db in Docker |
Path to the SQLite database file |
GITEA_PASSWORD |
(required for auto-deploy) |
Gitea password for agentbox user |
Usage
Registration
- Navigate to the app
- Click Register
- Choose a username and password
- Either join an existing household or create a new one
- The first user in a new household becomes the admin
Responding to Meals
- On the dashboard, click I'll be there (green) or I won't be there (red) for lunch and dinner
- Your response appears alongside other household members
- A timestamp records the last change between yes ↔ no
Admin Actions
- Remove a user: click the × button next to a member's name
- Delete household: use the dangerous action section (requires password confirmation)
- Delete your own account: available to all users, also requires password confirmation
API / Routes
Authentication
| Method |
Path |
Description |
| GET/POST |
/login |
Login page |
| GET/POST |
/register |
Registration page |
| GET |
/logout |
Logout (clears session) |
Meals
| Method |
Path |
Description |
| GET |
/ |
Redirects to dashboard or login |
| GET |
/dashboard |
Main dashboard (?date=YYYY-MM-DD to view other dates) |
| POST |
/respond |
Submit a meal response (meal_type, status, date) |
| GET |
/history |
History view (?date=YYYY-MM-DD) |
Account / Admin
| Method |
Path |
Description |
| POST |
/delete-account |
Delete own account (needs password) |
| POST |
/admin/remove-user/<id> |
Admin removes a household member |
| POST |
/admin/delete-household |
Admin deletes the entire household |
Database Schema
households
| Column |
Type |
Notes |
| id |
INTEGER |
PRIMARY KEY AUTOINCREMENT |
| name |
TEXT |
UNIQUE NOT NULL |
| created_at |
TIMESTAMP |
DEFAULT CURRENT_TIMESTAMP |
users
| Column |
Type |
Notes |
| id |
INTEGER |
PRIMARY KEY AUTOINCREMENT |
| username |
TEXT |
UNIQUE NOT NULL |
| password_hash |
TEXT |
NOT NULL |
| is_admin |
INTEGER |
DEFAULT 0 |
| household_id |
INTEGER |
REFERENCES households(id) |
| created_at |
TIMESTAMP |
DEFAULT CURRENT_TIMESTAMP |
meal_periods
| Column |
Type |
Notes |
| id |
INTEGER |
PRIMARY KEY AUTOINCREMENT |
| date |
TEXT |
NOT NULL (ISO format YYYY-MM-DD) |
| meal_type |
TEXT |
NOT NULL, CHECK(lunch OR dinner) |
|
|
UNIQUE(date, meal_type) |
responses
| Column |
Type |
Notes |
| id |
INTEGER |
PRIMARY KEY AUTOINCREMENT |
| user_id |
INTEGER |
NOT NULL, REFERENCES users(id) |
| period_id |
INTEGER |
NOT NULL, REFERENCES meal_periods(id) |
| status |
TEXT |
DEFAULT 'not_answered', CHECK(yes, no, not_answered) |
| changed_at |
TIMESTAMP |
Set when flipping between yes↔no |
| created_at |
TIMESTAMP |
DEFAULT CURRENT_TIMESTAMP |
|
|
UNIQUE(user_id, period_id) |
Running Tests
Tests use a temporary in-memory SQLite database (configured per test via conftest.py).