229 lines
9.3 KiB
Markdown
229 lines
9.3 KiB
Markdown
# 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
|
||
|
||
```
|
||
.
|
||
├── app.py # Flask application entry point
|
||
├── auth.py # Authentication blueprint (login, register, logout)
|
||
├── meals.py # Meals blueprint (dashboard, responses, history, admin)
|
||
├── models.py # Database models and helpers
|
||
├── i18n.py # Internationalization (en/fr/de)
|
||
├── ntfy.py # Ntfy notification helpers
|
||
├── requirements.txt # Python dependencies
|
||
├── Dockerfile # Container image definition
|
||
├── docker-compose.yml # Container orchestration
|
||
├── watch-for-updates.sh # Auto-deploy poller script
|
||
├── .gitea/ # Gitea CI/CD workflows
|
||
├── templates/ # Jinja2 HTML templates
|
||
│ ├── base.html
|
||
│ ├── login.html
|
||
│ ├── register.html
|
||
│ └── dashboard.html
|
||
├── static/
|
||
│ └── style.css
|
||
├── tests/ # Pytest test suite
|
||
│ ├── conftest.py
|
||
│ ├── test_auth.py
|
||
│ ├── test_meals.py
|
||
│ └── test_routes.py
|
||
└── doc/ # Documentation
|
||
└── index.md
|
||
```
|
||
|
||
## Getting Started
|
||
|
||
### Prerequisites
|
||
|
||
- Docker and docker-compose
|
||
- A Caddy reverse proxy network named `caddy` (already present on CozyTren infrastructure)
|
||
|
||
### Quick Start
|
||
|
||
```bash
|
||
# Clone the repository
|
||
git clone https://gitea.ct.cozytren.ch/romane/agentbox-test
|
||
cd agentbox-test
|
||
|
||
# Set your secret key (or it defaults to a dev value)
|
||
export SECRET_KEY="your-secret-here"
|
||
|
||
# Build and run
|
||
docker compose up -d --build
|
||
```
|
||
|
||
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:
|
||
|
||
```bash
|
||
# Source credentials and start the watcher
|
||
source .env
|
||
./watch-for-updates.sh
|
||
```
|
||
|
||
## 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
|
||
|
||
1. Navigate to the app
|
||
2. Click **Register**
|
||
3. Choose a username and password
|
||
4. Either **join** an existing household or **create** a new one
|
||
5. The first user in a new household becomes the admin
|
||
|
||
### Responding to Meals
|
||
|
||
1. On the dashboard, click **I'll be there** (green) or **I won't be there** (red) for lunch and dinner
|
||
2. Your response appears alongside other household members
|
||
3. 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
|
||
|
||
### Ntfy Callback (public)
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| POST | `/api/ntfy-callback` | Receive ntfy action callbacks. Auth via per-user `callback_token`. Body: `{"token":"...", "meal_type":"lunch|dinner", "date":"YYYY-MM-DD", "status":"yes|no"}`. Returns 200 on success, 403 for bad token, 400 for invalid/past data. |
|
||
|
||
### Ntfy Settings (authenticated)
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| POST | `/ntfy-settings` | Save ntfy topic URL and access token |
|
||
| POST | `/ntfy-test` | Send a test notification to verify configuration |
|
||
|
||
### 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
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
pytest tests/ -v
|
||
```
|
||
|
||
Tests use a temporary in-memory SQLite database (configured per test via `conftest.py`).
|
||
|
||
## Ntfy Notifications
|
||
|
||
Users can configure [ntfy](https://ntfy.sh) to receive push notifications with action buttons, allowing them to respond to meals directly from their phone without opening the web UI.
|
||
|
||
### Setup
|
||
|
||
1. Install the ntfy app on your phone and subscribe to a topic (e.g. `your-name-meals`)
|
||
2. (Optional) Create an access token for authenticated publishing
|
||
3. In the web UI, open **Account Settings** and enter:
|
||
- **Ntfy Topic URL**: Your topic (e.g. `https://ntfy.sh/your-name-meals`)
|
||
- **Access Token**: Your ntfy access token (if using auth)
|
||
4. Click **Send Test** to verify it works
|
||
|
||
### How It Works
|
||
|
||
- Reminders are sent automatically by the `meal-cron` container via `POST /api/cron-tick` every 60 seconds
|
||
- The notification contains **Home** and **Out** buttons
|
||
- Pressing a button sends an HTTP callback to the app, recording your response instantly
|
||
- Tapping the notification itself opens the dashboard
|
||
|
||
|