mirror of
https://github.com/sstent/foodplanner.git
synced 2026-03-28 08:05:23 +00:00
Compare commits
19 Commits
fe06c40a29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e411a8f6c4 | |||
| e68a1f52af | |||
| 7fc17967e0 | |||
| e47af2c839 | |||
| 7099577f92 | |||
| ced3c63f92 | |||
| 5c73ce9caf | |||
| 0e4cb5a5ed | |||
| b834e89a97 | |||
| 00600a76fa | |||
| cc6b4ca145 | |||
| f0430c810b | |||
| 326a82ea5d | |||
| afdf9fa5b7 | |||
| 8d3e91a825 | |||
| 042d8d93cc | |||
| a66a5142b5 | |||
| 7718a7f879 | |||
| 9574994abb |
59
.github/workflows/build-and-push-dev.yml
vendored
Normal file
59
.github/workflows/build-and-push-dev.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build and Push Docker Image (DEV)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
container_sha: ${{ github.sha }}
|
||||
registry_url: ${{ steps.registry.outputs.url }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set registry URL
|
||||
id: registry
|
||||
run: |
|
||||
if [ "${{ github.server_url }}" = "https://github.com" ]; then
|
||||
echo "url=ghcr.io" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "url=${{ github.server_url }}" | sed 's|https://||' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ steps.registry.outputs.url }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.PACKAGE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push multi-arch Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
${{ steps.registry.outputs.url }}/${{ github.repository }}:dev
|
||||
${{ steps.registry.outputs.url }}/${{ github.repository }}:${{ github.sha }}
|
||||
build-args: |
|
||||
COMMIT_SHA=${{ github.sha }}
|
||||
cache-from: type=registry,ref=${{ steps.registry.outputs.url }}/${{ github.repository }}:buildcache-dev
|
||||
cache-to: type=registry,ref=${{ steps.registry.outputs.url }}/${{ github.repository }}:buildcache-dev,mode=max
|
||||
|
||||
labels: org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
56
.github/workflows/nomad-deploy-dev.yml
vendored
Normal file
56
.github/workflows/nomad-deploy-dev.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Deploy to Nomad (DEV)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push Docker Image (DEV)"] # Must match your build workflow name exactly
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch: # Allows manual triggering for testing
|
||||
inputs:
|
||||
container_sha:
|
||||
description: 'Container SHA to deploy (leave empty for latest commit)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
nomad:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy to Nomad (DEV)
|
||||
# Only run if the build workflow succeeded
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
# 1. Checkout Code
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 2. Install Nomad CLI
|
||||
- name: Setup Nomad CLI
|
||||
uses: hashicorp/setup-nomad@main
|
||||
with:
|
||||
version: '1.10.0' # Use your desired version or remove for 'latest'
|
||||
|
||||
# 3. Determine container version to deploy
|
||||
- name: Set Container Version
|
||||
id: container_version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.container_sha }}" ]; then
|
||||
echo "sha=${{ inputs.container_sha }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||||
echo "sha=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# 4. Deploy the Nomad Job
|
||||
- name: Deploy Nomad Job
|
||||
id: deploy
|
||||
env:
|
||||
# REQUIRED: Set the Nomad server address
|
||||
NOMAD_ADDR: http://nomad.service.dc1.consul:4646
|
||||
run: |
|
||||
echo "Deploying container version: ${{ steps.container_version.outputs.sha }}"
|
||||
nomad status
|
||||
nomad job run \
|
||||
-var="container_version=${{ steps.container_version.outputs.sha }}" \
|
||||
foodplanner-dev.nomad
|
||||
27
alembic/versions/7fdcc454e056_add_name_to_tracked_meal.py
Normal file
27
alembic/versions/7fdcc454e056_add_name_to_tracked_meal.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""add_name_to_tracked_meal
|
||||
|
||||
Revision ID: 7fdcc454e056
|
||||
Revises: e1c2d8d5c1a8
|
||||
Create Date: 2026-02-24 06:29:46.441129
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7fdcc454e056'
|
||||
down_revision: Union[str, None] = 'e1c2d8d5c1a8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('tracked_meals', sa.Column('name', sa.String(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table('tracked_meals') as batch_op:
|
||||
batch_op.drop_column('name')
|
||||
@@ -15,7 +15,10 @@ router = APIRouter()
|
||||
@router.get("/meals", response_class=HTMLResponse)
|
||||
async def meals_page(request: Request, db: Session = Depends(get_db)):
|
||||
from sqlalchemy.orm import joinedload
|
||||
meals = db.query(Meal).options(joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
|
||||
# Filter out single food entries and snapshots
|
||||
meals = db.query(Meal).filter(
|
||||
Meal.meal_type.notin_(["single_food", "tracked_snapshot"])
|
||||
).options(joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
|
||||
foods = db.query(Food).all()
|
||||
return templates.TemplateResponse("meals.html",
|
||||
{"request": request, "meals": meals, "foods": foods})
|
||||
|
||||
@@ -746,23 +746,25 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
||||
# Store grams directly
|
||||
quantity = grams
|
||||
|
||||
# Create a new Meal for this single food entry
|
||||
# This allows it to be treated like any other meal in the tracker view
|
||||
new_meal = Meal(name=food_item.name, meal_type="single_food", meal_time=meal_time)
|
||||
db.add(new_meal)
|
||||
db.flush() # Flush to get the new meal ID
|
||||
|
||||
# Link the food to the new meal
|
||||
meal_food = MealFood(meal_id=new_meal.id, food_id=food_id, quantity=grams)
|
||||
db.add(meal_food)
|
||||
|
||||
# Create tracked meal entry
|
||||
# Create tracked meal entry without a parent Meal template
|
||||
tracked_meal = TrackedMeal(
|
||||
tracked_day_id=tracked_day.id,
|
||||
meal_id=new_meal.id,
|
||||
meal_time=meal_time
|
||||
meal_id=None,
|
||||
meal_time=meal_time,
|
||||
name=food_item.name
|
||||
)
|
||||
db.add(tracked_meal)
|
||||
db.flush() # Flush to get the tracked_meal ID
|
||||
|
||||
# Link the food directly to the tracked meal via TrackedMealFood
|
||||
new_entry = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=food_id,
|
||||
quantity=grams,
|
||||
is_override=False,
|
||||
is_deleted=False
|
||||
)
|
||||
db.add(new_entry)
|
||||
|
||||
# Mark day as modified
|
||||
tracked_day.is_modified = True
|
||||
|
||||
@@ -148,8 +148,9 @@ class TrackedMeal(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tracked_day_id = Column(Integer, ForeignKey("tracked_days.id"))
|
||||
meal_id = Column(Integer, ForeignKey("meals.id"))
|
||||
meal_id = Column(Integer, ForeignKey("meals.id"), nullable=True)
|
||||
meal_time = Column(String) # Breakfast, Lunch, Dinner, Snack 1, Snack 2, Beverage 1, Beverage 2
|
||||
name = Column(String, nullable=True) # For single food items or custom names
|
||||
|
||||
tracked_day = relationship("TrackedDay", back_populates="tracked_meals")
|
||||
meal = relationship("Meal")
|
||||
@@ -427,9 +428,11 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
|
||||
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
||||
}
|
||||
|
||||
# 1. Get base foods from the meal
|
||||
# 1. Get base foods from the meal (if it exists)
|
||||
# access via relationship, assume eager loading or lazy loading
|
||||
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
||||
base_foods = {}
|
||||
if tracked_meal.meal:
|
||||
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
||||
|
||||
# 2. Get tracked foods (overrides, deletions, additions)
|
||||
tracked_foods = tracked_meal.tracked_foods
|
||||
|
||||
5
conductor/archive/add_calcium_20260213/index.md
Normal file
5
conductor/archive/add_calcium_20260213/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Track add_calcium_20260213 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
8
conductor/archive/add_calcium_20260213/metadata.json
Normal file
8
conductor/archive/add_calcium_20260213/metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "add_calcium_20260213",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-02-13T00:00:00Z",
|
||||
"updated_at": "2026-02-13T00:00:00Z",
|
||||
"description": "Add Calcium bottom row of 'Daily Totals' on the tracker page"
|
||||
}
|
||||
18
conductor/archive/add_calcium_20260213/plan.md
Normal file
18
conductor/archive/add_calcium_20260213/plan.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan - Add Calcium to Tracker Totals
|
||||
|
||||
This plan follows the Test-Driven Development (TDD) process as outlined in `conductor/workflow.md`.
|
||||
|
||||
## Phase 1: Infrastructure and Red Phase
|
||||
- [x] Task: Create a failing E2E test for Calcium display
|
||||
- [ ] Define a new test in `tests/calcium_display.spec.js` that navigates to the tracker and expects a "Calcium" label and a numeric value in the Daily Totals section.
|
||||
- [ ] Execute the test and confirm it fails (Red Phase).
|
||||
|
||||
## Phase 2: Implementation (Green Phase) [checkpoint: 7718a7f]
|
||||
- [x] Task: Update tracker template to include Calcium
|
||||
- [ ] Modify `templates/tracker.html` to add a fourth column to the third row of the "Daily Totals" card.
|
||||
- [ ] Update existing `col-4` classes in that row to `col-3` to accommodate the new column.
|
||||
- [ ] Bind the display to `day_totals.calcium` with a `0` decimal place filter and "mg" unit.
|
||||
- [x] Task: Verify implementation
|
||||
- [ ] Execute the E2E test created in Phase 1 and confirm it passes (Green Phase).
|
||||
- [ ] Run existing backend tests to ensure no regressions in nutrition calculations.
|
||||
- [x] Task: Conductor - User Manual Verification 'Implementation' (Protocol in workflow.md)
|
||||
25
conductor/archive/add_calcium_20260213/spec.md
Normal file
25
conductor/archive/add_calcium_20260213/spec.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Specification - Add Calcium to Tracker Totals
|
||||
|
||||
## Overview
|
||||
This track adds a Calcium display to the "Daily Totals" section of the tracker page. This allows users to track their calcium intake (in mg) alongside other micronutrients and macronutrients.
|
||||
|
||||
## Functional Requirements
|
||||
- **Display Calcium:** Add a new column to the third row of the "Daily Totals" card on the `tracker.html` page.
|
||||
- **Unit of Measurement:** Calcium shall be displayed in milligrams (mg).
|
||||
- **Precision:** Calcium values shall be rounded to the nearest whole number (0 decimal places).
|
||||
- **Data Source:** Use the `day_totals.calcium` value provided by the backend, which is already correctly calculated in `calculate_day_nutrition_tracked`.
|
||||
|
||||
## Non-Functional Requirements
|
||||
- **UI Consistency:** The new Calcium display should match the style of the existing Sugar, Fiber, and Sodium displays (border, padding, centered text, small muted label).
|
||||
- **Responsiveness:** The layout should remain functional on various screen sizes. Adding a fourth column to the row may require adjusting column widths (e.g., from `col-4` to `col-3`).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] A "Calcium" box is visible in the "Daily Totals" section of the tracker page.
|
||||
- [ ] The value displayed matches the sum of calcium from all tracked foods for that day.
|
||||
- [ ] The unit "mg" is displayed next to or below the value.
|
||||
- [ ] The layout of the "Daily Totals" card remains clean and balanced.
|
||||
|
||||
## Out of Scope
|
||||
- Adding Calcium to individual food or meal breakdown tables.
|
||||
- Updating Open Food Facts import logic (unless found to be missing Calcium data during verification).
|
||||
- Adding Calcium targets or progress bars.
|
||||
49
conductor/code_styleguides/html-css.md
Normal file
49
conductor/code_styleguides/html-css.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Google HTML/CSS Style Guide Summary
|
||||
|
||||
This document summarizes key rules and best practices from the Google HTML/CSS Style Guide.
|
||||
|
||||
## 1. General Rules
|
||||
- **Protocol:** Use HTTPS for all embedded resources.
|
||||
- **Indentation:** Indent by 2 spaces. Do not use tabs.
|
||||
- **Capitalization:** Use only lowercase for all code (element names, attributes, selectors, properties).
|
||||
- **Trailing Whitespace:** Remove all trailing whitespace.
|
||||
- **Encoding:** Use UTF-8 (without a BOM). Specify `<meta charset="utf-8">` in HTML.
|
||||
|
||||
## 2. HTML Style Rules
|
||||
- **Document Type:** Use `<!doctype html>`.
|
||||
- **HTML Validity:** Use valid HTML.
|
||||
- **Semantics:** Use HTML elements according to their intended purpose (e.g., use `<p>` for paragraphs, not for spacing).
|
||||
- **Multimedia Fallback:** Provide `alt` text for images and transcripts/captions for audio/video.
|
||||
- **Separation of Concerns:** Strictly separate structure (HTML), presentation (CSS), and behavior (JavaScript). Link to CSS and JS from external files.
|
||||
- **`type` Attributes:** Omit `type` attributes for stylesheets (`<link>`) and scripts (`<script>`).
|
||||
|
||||
## 3. HTML Formatting Rules
|
||||
- **General:** Use a new line for every block, list, or table element, and indent its children.
|
||||
- **Quotation Marks:** Use double quotation marks (`""`) for attribute values.
|
||||
|
||||
## 4. CSS Style Rules
|
||||
- **CSS Validity:** Use valid CSS.
|
||||
- **Class Naming:** Use meaningful, generic names. Separate words with a hyphen (`-`).
|
||||
- **Good:** `.video-player`, `.site-navigation`
|
||||
- **Bad:** `.vid`, `.red-text`
|
||||
- **ID Selectors:** Avoid using ID selectors for styling. Prefer class selectors.
|
||||
- **Shorthand Properties:** Use shorthand properties where possible (e.g., `padding`, `font`).
|
||||
- **`0` and Units:** Omit units for `0` values (e.g., `margin: 0;`).
|
||||
- **Leading `0`s:** Always include leading `0`s for decimal values (e.g., `font-size: 0.8em;`).
|
||||
- **Hexadecimal Notation:** Use 3-character hex notation where possible (e.g., `#fff`).
|
||||
- **`!important`:** Avoid using `!important`.
|
||||
|
||||
## 5. CSS Formatting Rules
|
||||
- **Declaration Order:** Alphabetize declarations within a rule.
|
||||
- **Indentation:** Indent all block content.
|
||||
- **Semicolons:** Use a semicolon after every declaration.
|
||||
- **Spacing:**
|
||||
- Use a space after a property name's colon (`font-weight: bold;`).
|
||||
- Use a space between the last selector and the opening brace (`.foo {`).
|
||||
- Start a new line for each selector and declaration.
|
||||
- **Rule Separation:** Separate rules with a new line.
|
||||
- **Quotation Marks:** Use single quotes (`''`) for attribute selectors and property values (e.g., `[type='text']`).
|
||||
|
||||
**BE CONSISTENT.** When editing code, match the existing style.
|
||||
|
||||
*Source: [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)*
|
||||
51
conductor/code_styleguides/javascript.md
Normal file
51
conductor/code_styleguides/javascript.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Google JavaScript Style Guide Summary
|
||||
|
||||
This document summarizes key rules and best practices from the Google JavaScript Style Guide.
|
||||
|
||||
## 1. Source File Basics
|
||||
- **File Naming:** All lowercase, with underscores (`_`) or dashes (`-`). Extension must be `.js`.
|
||||
- **File Encoding:** UTF-8.
|
||||
- **Whitespace:** Use only ASCII horizontal spaces (0x20). Tabs are forbidden for indentation.
|
||||
|
||||
## 2. Source File Structure
|
||||
- New files should be ES modules (`import`/`export`).
|
||||
- **Exports:** Use named exports (`export {MyClass};`). **Do not use default exports.**
|
||||
- **Imports:** Do not use line-wrapped imports. The `.js` extension in import paths is mandatory.
|
||||
|
||||
## 3. Formatting
|
||||
- **Braces:** Required for all control structures (`if`, `for`, `while`, etc.), even single-line blocks. Use K&R style ("Egyptian brackets").
|
||||
- **Indentation:** +2 spaces for each new block.
|
||||
- **Semicolons:** Every statement must be terminated with a semicolon.
|
||||
- **Column Limit:** 80 characters.
|
||||
- **Line-wrapping:** Indent continuation lines at least +4 spaces.
|
||||
- **Whitespace:** Use single blank lines between methods. No trailing whitespace.
|
||||
|
||||
## 4. Language Features
|
||||
- **Variable Declarations:** Use `const` by default, `let` if reassignment is needed. **`var` is forbidden.**
|
||||
- **Array Literals:** Use trailing commas. Do not use the `Array` constructor.
|
||||
- **Object Literals:** Use trailing commas and shorthand properties. Do not use the `Object` constructor.
|
||||
- **Classes:** Do not use JavaScript getter/setter properties (`get name()`). Provide ordinary methods instead.
|
||||
- **Functions:** Prefer arrow functions for nested functions to preserve `this` context.
|
||||
- **String Literals:** Use single quotes (`'`). Use template literals (`` ` ``) for multi-line strings or complex interpolation.
|
||||
- **Control Structures:** Prefer `for-of` loops. `for-in` loops should only be used on dict-style objects.
|
||||
- **`this`:** Only use `this` in class constructors, methods, or in arrow functions defined within them.
|
||||
- **Equality Checks:** Always use identity operators (`===` / `!==`).
|
||||
|
||||
## 5. Disallowed Features
|
||||
- `with` keyword.
|
||||
- `eval()` or `Function(...string)`.
|
||||
- Automatic Semicolon Insertion.
|
||||
- Modifying builtin objects (`Array.prototype.foo = ...`).
|
||||
|
||||
## 6. Naming
|
||||
- **Classes:** `UpperCamelCase`.
|
||||
- **Methods & Functions:** `lowerCamelCase`.
|
||||
- **Constants:** `CONSTANT_CASE` (all uppercase with underscores).
|
||||
- **Non-constant Fields & Variables:** `lowerCamelCase`.
|
||||
|
||||
## 7. JSDoc
|
||||
- JSDoc is used on all classes, fields, and methods.
|
||||
- Use `@param`, `@return`, `@override`, `@deprecated`.
|
||||
- Type annotations are enclosed in braces (e.g., `/** @param {string} userName */`).
|
||||
|
||||
*Source: [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)*
|
||||
37
conductor/code_styleguides/python.md
Normal file
37
conductor/code_styleguides/python.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Google Python Style Guide Summary
|
||||
|
||||
This document summarizes key rules and best practices from the Google Python Style Guide.
|
||||
|
||||
## 1. Python Language Rules
|
||||
- **Linting:** Run `pylint` on your code to catch bugs and style issues.
|
||||
- **Imports:** Use `import x` for packages/modules. Use `from x import y` only when `y` is a submodule.
|
||||
- **Exceptions:** Use built-in exception classes. Do not use bare `except:` clauses.
|
||||
- **Global State:** Avoid mutable global state. Module-level constants are okay and should be `ALL_CAPS_WITH_UNDERSCORES`.
|
||||
- **Comprehensions:** Use for simple cases. Avoid for complex logic where a full loop is more readable.
|
||||
- **Default Argument Values:** Do not use mutable objects (like `[]` or `{}`) as default values.
|
||||
- **True/False Evaluations:** Use implicit false (e.g., `if not my_list:`). Use `if foo is None:` to check for `None`.
|
||||
- **Type Annotations:** Strongly encouraged for all public APIs.
|
||||
|
||||
## 2. Python Style Rules
|
||||
- **Line Length:** Maximum 80 characters.
|
||||
- **Indentation:** 4 spaces per indentation level. Never use tabs.
|
||||
- **Blank Lines:** Two blank lines between top-level definitions (classes, functions). One blank line between method definitions.
|
||||
- **Whitespace:** Avoid extraneous whitespace. Surround binary operators with single spaces.
|
||||
- **Docstrings:** Use `"""triple double quotes"""`. Every public module, function, class, and method must have a docstring.
|
||||
- **Format:** Start with a one-line summary. Include `Args:`, `Returns:`, and `Raises:` sections.
|
||||
- **Strings:** Use f-strings for formatting. Be consistent with single (`'`) or double (`"`) quotes.
|
||||
- **`TODO` Comments:** Use `TODO(username): Fix this.` format.
|
||||
- **Imports Formatting:** Imports should be on separate lines and grouped: standard library, third-party, and your own application's imports.
|
||||
|
||||
## 3. Naming
|
||||
- **General:** `snake_case` for modules, functions, methods, and variables.
|
||||
- **Classes:** `PascalCase`.
|
||||
- **Constants:** `ALL_CAPS_WITH_UNDERSCORES`.
|
||||
- **Internal Use:** Use a single leading underscore (`_internal_variable`) for internal module/class members.
|
||||
|
||||
## 4. Main
|
||||
- All executable files should have a `main()` function that contains the main logic, called from a `if __name__ == '__main__':` block.
|
||||
|
||||
**BE CONSISTENT.** When editing code, match the existing style.
|
||||
|
||||
*Source: [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)*
|
||||
14
conductor/index.md
Normal file
14
conductor/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Project Context
|
||||
|
||||
## Definition
|
||||
- [Product Definition](./product.md)
|
||||
- [Product Guidelines](./product-guidelines.md)
|
||||
- [Tech Stack](./tech-stack.md)
|
||||
|
||||
## Workflow
|
||||
- [Workflow](./workflow.md)
|
||||
- [Code Style Guides](./code_styleguides/)
|
||||
|
||||
## Management
|
||||
- [Tracks Registry](./tracks.md)
|
||||
- [Tracks Directory](./tracks/)
|
||||
16
conductor/product-guidelines.md
Normal file
16
conductor/product-guidelines.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Product Guidelines - FoodPlanner
|
||||
|
||||
## Visual Identity & Tone
|
||||
- **Minimalist and Focused:** The interface should be clean and distraction-free, highlighting one primary task at a time to reduce cognitive load.
|
||||
- **Data Clarity:** Prioritize a high-contrast, readable layout that makes nutritional values easy to scan.
|
||||
|
||||
## User Interface Principles
|
||||
- **Page-Specific Nutritional Awareness:**
|
||||
- **Global Context:** Use a subtle, persistent summary (e.g., a sticky header) to keep total macros/calories visible for continuous goal tracking.
|
||||
- **Local Context:** On list-heavy pages (like Food Search or Meal Building), integrate nutritional data directly with the items for immediate decision-making.
|
||||
- **Task-Oriented "Empty States":** When a user encounters an empty view (no foods, no plans), provide clear, actionable instructions and buttons within that space to guide them to the next step.
|
||||
|
||||
## Interaction & Behavior
|
||||
- **Immediate & Reactive:** Every user action (adjusting quantities, adding items) must trigger an instant UI update to the relevant nutritional totals.
|
||||
- **Unambiguous Validation:** Use Modal Alerts for errors that require immediate attention or could result in data loss, ensuring critical issues are never overlooked.
|
||||
- **Low Friction:** Design interactions to minimize the number of clicks required for common tasks like portion adjustments or food selection.
|
||||
26
conductor/product.md
Normal file
26
conductor/product.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Initial Concept\nAn existing Meal Planner application built with FastAPI, SQLAlchemy, and Jinja2 for managing foods, meals, and nutritional plans.
|
||||
|
||||
# Product Definition - FoodPlanner
|
||||
|
||||
## Vision
|
||||
A performance-oriented meal planning application designed for health-conscious households (specifically 2-person units) to hit precise nutritional and macronutrient targets. The application prioritizes speed, data reliability, and structured planning over complex external integrations.
|
||||
|
||||
## Target Users
|
||||
- **Macro-Trackers:** Individuals who meticulously track their caloric and macronutrient intake.
|
||||
- **2-Person Households:** Users who need to coordinate meal plans for a small, consistent group.
|
||||
|
||||
## Core Goals
|
||||
- **Nutritional Precision:** Enable users to reach specific daily and weekly macro/calorie goals through structured 2-week planning.
|
||||
- **Efficiency:** Reduce the friction of meal prep by providing tools to save, reuse, and template successful plans and meals.
|
||||
- **Reliability:** Provide a fast, local-first experience with robust data storage (SQLite/PostgreSQL).
|
||||
|
||||
## Key Features
|
||||
- **Nutritional Totaling:** Automated, real-time calculation of macros and calories for individual foods, combined meals, and full daily/weekly plans.
|
||||
- **Detailed Daily Planner:** A granular view of each day to visualize meal distribution and ensure macro balance across the day.
|
||||
- **Meals Library:** A centralized repository to create and save custom meals from individual food components.
|
||||
- **Template System:** Save and apply successful daily or weekly structures to future plans to minimize repetitive data entry.
|
||||
- **Open Food Facts Integration:** Rapidly expand the local food database by importing data directly from the Open Food Facts API.
|
||||
|
||||
## Technical Philosophy
|
||||
- **Local-First & Fast:** The UI must be highly responsive, prioritizing a smooth user experience for frequent data entry.
|
||||
- **Structured Data:** Use a relational database to ensure data integrity across foods, meals, and plans.
|
||||
1
conductor/setup_state.json
Normal file
1
conductor/setup_state.json
Normal file
@@ -0,0 +1 @@
|
||||
{"last_successful_step": "3.3_initial_track_generated"}
|
||||
23
conductor/tech-stack.md
Normal file
23
conductor/tech-stack.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Tech Stack - FoodPlanner
|
||||
|
||||
## Core Backend
|
||||
- **Language:** Python 3.7+
|
||||
- **Web Framework:** FastAPI (Asynchronous, high-performance web framework)
|
||||
- **Data Validation:** Pydantic (Used for schema definition and request/response validation)
|
||||
|
||||
## Data Layer
|
||||
- **ORM:** SQLAlchemy (Using 2.0+ patterns for database interactions)
|
||||
- **Database:** Support for SQLite (local development) and PostgreSQL (production)
|
||||
- **Migrations:** Alembic (Handles database schema evolution)
|
||||
|
||||
## Frontend & UI
|
||||
- **Templating:** Jinja2 (Server-side rendering for HTML templates)
|
||||
- **Frontend Logic:** Minimal JavaScript for reactive UI updates and modal management
|
||||
|
||||
## External Integrations
|
||||
- **Food Data:** Open Food Facts API (For importing nutritional information)
|
||||
- **AI/Extraction:** OpenAI API (Used for extracting food data from unstructured text)
|
||||
|
||||
## Quality Assurance
|
||||
- **E2E Testing:** Playwright (For browser-based integration tests)
|
||||
- **Unit/Integration Testing:** pytest (For backend logic and API testing)
|
||||
8
conductor/tracks.md
Normal file
8
conductor/tracks.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Project Tracks
|
||||
|
||||
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
|
||||
|
||||
---
|
||||
|
||||
- [x] **Track: Refactor the meal tracking system to decouple 'Journal Logs' from 'Cookbook Recipes'**
|
||||
*Link: [./tracks/meal_tracker_refactor_20250223/](./tracks/meal_tracker_refactor_20250223/)*
|
||||
5
conductor/tracks/add_calcium_20260213/index.md
Normal file
5
conductor/tracks/add_calcium_20260213/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Track add_calcium_20260213 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
8
conductor/tracks/add_calcium_20260213/metadata.json
Normal file
8
conductor/tracks/add_calcium_20260213/metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "add_calcium_20260213",
|
||||
"type": "feature",
|
||||
"status": "new",
|
||||
"created_at": "2026-02-13T00:00:00Z",
|
||||
"updated_at": "2026-02-13T00:00:00Z",
|
||||
"description": "Add Calcium bottom row of 'Daily Totals' on the tracker page"
|
||||
}
|
||||
18
conductor/tracks/add_calcium_20260213/plan.md
Normal file
18
conductor/tracks/add_calcium_20260213/plan.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Implementation Plan - Add Calcium to Tracker Totals
|
||||
|
||||
This plan follows the Test-Driven Development (TDD) process as outlined in `conductor/workflow.md`.
|
||||
|
||||
## Phase 1: Infrastructure and Red Phase
|
||||
- [x] Task: Create a failing E2E test for Calcium display
|
||||
- [ ] Define a new test in `tests/calcium_display.spec.js` that navigates to the tracker and expects a "Calcium" label and a numeric value in the Daily Totals section.
|
||||
- [ ] Execute the test and confirm it fails (Red Phase).
|
||||
|
||||
## Phase 2: Implementation (Green Phase) [checkpoint: 7718a7f]
|
||||
- [x] Task: Update tracker template to include Calcium
|
||||
- [ ] Modify `templates/tracker.html` to add a fourth column to the third row of the "Daily Totals" card.
|
||||
- [ ] Update existing `col-4` classes in that row to `col-3` to accommodate the new column.
|
||||
- [ ] Bind the display to `day_totals.calcium` with a `0` decimal place filter and "mg" unit.
|
||||
- [x] Task: Verify implementation
|
||||
- [ ] Execute the E2E test created in Phase 1 and confirm it passes (Green Phase).
|
||||
- [ ] Run existing backend tests to ensure no regressions in nutrition calculations.
|
||||
- [x] Task: Conductor - User Manual Verification 'Implementation' (Protocol in workflow.md)
|
||||
25
conductor/tracks/add_calcium_20260213/spec.md
Normal file
25
conductor/tracks/add_calcium_20260213/spec.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Specification - Add Calcium to Tracker Totals
|
||||
|
||||
## Overview
|
||||
This track adds a Calcium display to the "Daily Totals" section of the tracker page. This allows users to track their calcium intake (in mg) alongside other micronutrients and macronutrients.
|
||||
|
||||
## Functional Requirements
|
||||
- **Display Calcium:** Add a new column to the third row of the "Daily Totals" card on the `tracker.html` page.
|
||||
- **Unit of Measurement:** Calcium shall be displayed in milligrams (mg).
|
||||
- **Precision:** Calcium values shall be rounded to the nearest whole number (0 decimal places).
|
||||
- **Data Source:** Use the `day_totals.calcium` value provided by the backend, which is already correctly calculated in `calculate_day_nutrition_tracked`.
|
||||
|
||||
## Non-Functional Requirements
|
||||
- **UI Consistency:** The new Calcium display should match the style of the existing Sugar, Fiber, and Sodium displays (border, padding, centered text, small muted label).
|
||||
- **Responsiveness:** The layout should remain functional on various screen sizes. Adding a fourth column to the row may require adjusting column widths (e.g., from `col-4` to `col-3`).
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] A "Calcium" box is visible in the "Daily Totals" section of the tracker page.
|
||||
- [ ] The value displayed matches the sum of calcium from all tracked foods for that day.
|
||||
- [ ] The unit "mg" is displayed next to or below the value.
|
||||
- [ ] The layout of the "Daily Totals" card remains clean and balanced.
|
||||
|
||||
## Out of Scope
|
||||
- Adding Calcium to individual food or meal breakdown tables.
|
||||
- Updating Open Food Facts import logic (unless found to be missing Calcium data during verification).
|
||||
- Adding Calcium targets or progress bars.
|
||||
5
conductor/tracks/meal_tracker_refactor_20250223/index.md
Normal file
5
conductor/tracks/meal_tracker_refactor_20250223/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Track meal_tracker_refactor_20250223 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"track_id": "meal_tracker_refactor_20250223",
|
||||
"type": "refactor",
|
||||
"status": "new",
|
||||
"created_at": "2025-02-23T12:00:00Z",
|
||||
"updated_at": "2025-02-23T12:00:00Z",
|
||||
"description": "Refactor the meal tracking system to decouple 'Journal Logs' from 'Cookbook Recipes', resolving database pollution and improving system structure."
|
||||
}
|
||||
28
conductor/tracks/meal_tracker_refactor_20250223/plan.md
Normal file
28
conductor/tracks/meal_tracker_refactor_20250223/plan.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Implementation Plan - Meal Tracker Refactoring
|
||||
|
||||
This plan outlines the steps for refactoring the meal tracking system to decouple "Journal Logs" from "Cookbook Recipes," resolving database pollution and improving system structure.
|
||||
|
||||
## Phase 1: Preparation & Schema Updates [checkpoint: 326a82e]
|
||||
- [x] Task: Create a new branch for the refactoring track.
|
||||
- [x] Task: Add the 'name' column to the 'TrackedMeal' table and make 'meal_id' nullable in 'app/database.py'.
|
||||
- [x] Task: Create and run an Alembic migration for the schema changes.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Schema Updates' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Logic & Calculation Updates [checkpoint: cc6b4ca]
|
||||
- [ ] Task: Write failing unit tests for 'calculate_tracked_meal_nutrition' with 'meal_id=None'.
|
||||
- [ ] Task: Implement support for 'meal_id=None' in 'calculate_tracked_meal_nutrition' within 'app/database.py'.
|
||||
- [ ] Task: Write failing unit tests for the refactored 'tracker_add_food' endpoint.
|
||||
- [ ] Task: Refactor the 'tracker_add_food' route in 'app/api/routes/tracker.py' to use the new 'TrackedMeal' structure.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Logic & Calculation Updates' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: UI & Cookbook Refinement [checkpoint: b834e89]
|
||||
- [ ] Task: Update the 'tracker.html' template to display 'TrackedMeal.name' for template-less logs.
|
||||
- [ ] Task: Update the Meals page in 'app/api/routes/meals.py' to filter out 'single_food' and 'snapshot' types.
|
||||
- [ ] Task: Write failing E2E tests for the new tracking workflow.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI & Cookbook Refinement' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Database Migration & Cleanup [checkpoint: 5c73ce9]
|
||||
- [x] Task: Create a Python migration script for cleaning up existing 'single_food' entries.
|
||||
- [x] Task: Run the migration script on the development PostgreSQL database.
|
||||
- [x] Task: Verify the database state and ensure no orphans remain.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 4: Database Migration & Cleanup' (Protocol in workflow.md)
|
||||
28
conductor/tracks/meal_tracker_refactor_20250223/spec.md
Normal file
28
conductor/tracks/meal_tracker_refactor_20250223/spec.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Specification - Meal Tracker Refactoring
|
||||
|
||||
**Overview:**
|
||||
Refactor the meal tracking system to decouple "Journal Logs" from "Cookbook Recipes". Currently, adding a single food item via the tracker incorrectly creates a permanent 'Meal' record of type 'single_food', leading to database pollution and duplicate entries in the Meals library.
|
||||
|
||||
**Functional Requirements:**
|
||||
- **TrackedMeal Schema Update:** Add a 'name' column to the 'TrackedMeal' model to store the display name of a logged meal or a single food item.
|
||||
- **Nullable meal_id:** Modify 'TrackedMeal.meal_id' to be nullable, allowing "template-less" logs.
|
||||
- **Refactored Tracker Logic:** Update the 'tracker_add_food' route to log single items directly as a 'TrackedMeal' with 'meal_id=NULL' and the 'name' set to the food item's name.
|
||||
- **Nutrition Calculation:** Update nutrition calculation logic to handle 'TrackedMeal' entries without a parent 'Meal' template.
|
||||
- **Tracker UI Update:** Ensure the tracker page displays 'TrackedMeal.name' for these logs and maintains the seamless visual style of existing entries.
|
||||
- **Cookbook Cleanup (One-time Migration):** Migrate existing 'single_food' meals to the new format and purge the redundant records from the 'meals' and 'meal_foods' tables.
|
||||
- **Cookbook Filtering:** Update the Meals page to exclude 'single_food' and 'snapshot' meal types from view.
|
||||
|
||||
**Non-Functional Requirements:**
|
||||
- **Database Integrity:** Ensure all existing logs remain accurate and correctly linked to their food items during migration.
|
||||
- **Performance:** The tracker page should remain fast and responsive with the new logic.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Adding a single food to the tracker does **not** create a new entry in the 'meals' table.
|
||||
- [ ] Existing 'single_food' duplicates are removed from the 'meals' library.
|
||||
- [ ] The Meals page only shows "Cookbook Recipes" (e.g., proper combined meals).
|
||||
- [ ] The Tracker page correctly displays names and calculates nutrition for all logs (both template-based and template-less).
|
||||
- [ ] "Save as New Meal" remains available for all log entries, including single foods.
|
||||
|
||||
**Out of Scope:**
|
||||
- Refactoring the entire meal planning system beyond the tracker/cookbook separation.
|
||||
- Changes to the external Open Food Facts integration.
|
||||
169
conductor/workflow.md
Normal file
169
conductor/workflow.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Project Workflow
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
1. **The Plan is the Source of Truth:** All work must be tracked in `plan.md`
|
||||
2. **The Tech Stack is Deliberate:** Changes to the tech stack must be documented in `tech-stack.md` *before* implementation
|
||||
3. **Test-Driven Development:** Write unit tests before implementing functionality
|
||||
4. **High Code Coverage:** Aim for >80% code coverage for all modules
|
||||
5. **User Experience First:** Every decision should prioritize user experience
|
||||
6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution.
|
||||
|
||||
## Task Workflow
|
||||
|
||||
All tasks follow a strict lifecycle:
|
||||
|
||||
### Standard Task Workflow
|
||||
|
||||
1. **Select Task:** Choose the next available task from `plan.md` in sequential order
|
||||
|
||||
2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]`
|
||||
|
||||
3. **Write Failing Tests (Red Phase):**
|
||||
- Create a new test file for the feature or bug fix.
|
||||
- Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task.
|
||||
- **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests.
|
||||
|
||||
4. **Implement to Pass Tests (Green Phase):**
|
||||
- Write the minimum amount of application code necessary to make the failing tests pass.
|
||||
- Run the test suite again and confirm that all tests now pass. This is the "Green" phase.
|
||||
|
||||
5. **Refactor (Optional but Recommended):**
|
||||
- With the safety of passing tests, refactor the implementation code and the test code to improve clarity, remove duplication, and enhance performance without changing the external behavior.
|
||||
- Rerun tests to ensure they still pass after refactoring.
|
||||
|
||||
6. **Verify Coverage:** Run coverage reports using the project's chosen tools (e.g., `pytest --cov=app`). Target: >80% coverage for new code.
|
||||
|
||||
7. **Document Deviations:** If implementation differs from tech stack:
|
||||
- **STOP** implementation
|
||||
- Update `tech-stack.md` with new design
|
||||
- Add dated note explaining the change
|
||||
- Resume implementation
|
||||
|
||||
8. **Record Completion in Plan:** Update `plan.md`, find the line for the completed task, and update its status from `[~]` to `[x]`.
|
||||
*Note: Committing code and plan updates is deferred until the entire phase is complete.*
|
||||
|
||||
### Phase Completion Verification and Checkpointing Protocol
|
||||
|
||||
**Trigger:** This protocol is executed immediately after a task is completed that also concludes a phase in `plan.md`.
|
||||
|
||||
1. **Announce Protocol Start:** Inform the user that the phase is complete and the verification and checkpointing protocol has begun.
|
||||
|
||||
2. **Ensure Test Coverage for Phase Changes:**
|
||||
- **Step 2.1: Determine Phase Scope:** Identify files changed since the last phase checkpoint.
|
||||
- **Step 2.2: List Changed Files:** Execute `git diff --name-only <previous_checkpoint_sha> HEAD` (or since first commit if no previous checkpoint).
|
||||
- **Step 2.3: Verify and Create Tests:** Ensure all new code has corresponding tests following project conventions.
|
||||
|
||||
3. **Execute Automated Tests with Proactive Debugging:**
|
||||
- Announce and run the automated test suite (e.g., `pytest` and `playwright test`).
|
||||
- If tests fail, debug and fix (maximum two attempts before seeking guidance).
|
||||
|
||||
4. **Propose a Detailed, Actionable Manual Verification Plan:**
|
||||
- Analyze `product.md`, `product-guidelines.md`, and `plan.md` to define user-facing goals.
|
||||
- Present a step-by-step verification plan with expected outcomes.
|
||||
|
||||
5. **Await Explicit User Feedback:**
|
||||
- Pause for the user to confirm: "Does this meet your expectations?"
|
||||
|
||||
6. **Create Phase Checkpoint Commit:**
|
||||
- Stage all code changes and the updated `plan.md`.
|
||||
- Perform a single commit for the entire phase with a message like `feat(phase): Complete <Phase Name>`.
|
||||
|
||||
7. **Attach Consolidated Task Summary using Git Notes:**
|
||||
- **Step 7.1: Draft Note Content:** Create a detailed summary for *all* tasks completed in this phase, including verification results.
|
||||
- **Step 7.2: Attach Note:** Use `git notes add -m "<summary>" <checkpoint_commit_hash>`.
|
||||
|
||||
8. **Get and Record Phase Checkpoint SHA:**
|
||||
- Obtain the hash of the checkpoint commit and update the phase heading in `plan.md` with `[checkpoint: <sha>]`.
|
||||
|
||||
9. **Commit Plan Update:**
|
||||
- Stage `plan.md` and commit with `conductor(plan): Mark phase '<PHASE NAME>' as complete`.
|
||||
|
||||
10. **Announce Completion:** Inform the user that the phase is complete and the checkpoint has been created.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Before marking any task complete, verify:
|
||||
|
||||
- [ ] All tests pass
|
||||
- [ ] Code coverage meets requirements (>80%)
|
||||
- [ ] Code follows project's code style guidelines
|
||||
- [ ] All public functions/methods are documented
|
||||
- [ ] Type safety is enforced (Pydantic models, type hints)
|
||||
- [ ] No linting or static analysis errors
|
||||
- [ ] Documentation updated if needed
|
||||
- [ ] No security vulnerabilities introduced
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
npm install
|
||||
```
|
||||
|
||||
### Daily Development
|
||||
```bash
|
||||
# Start FastAPI server
|
||||
uvicorn main:app --reload
|
||||
|
||||
# Run Backend Tests
|
||||
pytest
|
||||
|
||||
# Run E2E Tests
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
### Before Committing
|
||||
```bash
|
||||
# Run all checks
|
||||
pytest && npx playwright test
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Testing
|
||||
- Every module must have corresponding tests in `tests/`.
|
||||
- Use `pytest` fixtures for database and app setup.
|
||||
- Mock external APIs (Open Food Facts, OpenAI).
|
||||
|
||||
### Integration Testing
|
||||
- Test complete API flows using `httpx`.
|
||||
- Verify database state after operations.
|
||||
|
||||
### E2E Testing
|
||||
- Use Playwright to test critical user journeys (Creating Plans, Adding Foods).
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
### Message Format
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### Types
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `style`: Formatting, missing semicolons, etc.
|
||||
- `refactor`: Code change that neither fixes a bug nor adds a feature
|
||||
- `test`: Adding missing tests
|
||||
- `chore`: Maintenance tasks
|
||||
|
||||
## Definition of Done
|
||||
|
||||
A task is complete when:
|
||||
|
||||
1. All code implemented to specification
|
||||
2. Unit tests written and passing
|
||||
3. Code coverage meets project requirements
|
||||
4. Documentation complete (if applicable)
|
||||
5. Code passes all configured linting and static analysis checks
|
||||
6. Implementation notes recorded for the phase summary
|
||||
7. Phase changes committed and summarized with Git Notes
|
||||
@@ -4,8 +4,8 @@ services:
|
||||
ports:
|
||||
- "8999:8999"
|
||||
environment:
|
||||
#- DATABASE_URL=sqlite:////app/data/meal_planner.db
|
||||
- DATABASE_URL=postgresql://postgres:postgres@master.postgres.service.dc1.consul/meal_planner_dev
|
||||
- DATABASE_URL=sqlite:////app/data/meal_planner.db
|
||||
#- DATABASE_URL=postgresql://postgres:postgres@master.postgres.service.dc1.consul/meal_planner_dev
|
||||
- PYTHONUNBUFFERED=1
|
||||
volumes:
|
||||
- ./alembic:/app/alembic
|
||||
|
||||
59
foodplanner-dev.nomad
Normal file
59
foodplanner-dev.nomad
Normal file
@@ -0,0 +1,59 @@
|
||||
variable "container_version" {
|
||||
default = "dev"
|
||||
}
|
||||
|
||||
job "foodplanner-dev" {
|
||||
datacenters = ["dc1"]
|
||||
|
||||
type = "service"
|
||||
|
||||
group "app" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
port "http" {
|
||||
to = 8999
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
name = "foodplanner-dev"
|
||||
port = "http"
|
||||
|
||||
check {
|
||||
type = "http"
|
||||
path = "/"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task "app" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "ghcr.io/sstent/foodplanner:${var.container_version}"
|
||||
ports = ["http"]
|
||||
}
|
||||
env {
|
||||
DATABASE_URL = "postgresql://postgres:postgres@master.postgres.service.dc1.consul/meal_planner_dev"
|
||||
|
||||
}
|
||||
resources {
|
||||
cpu = 500
|
||||
memory = 1024
|
||||
}
|
||||
|
||||
# Restart policy
|
||||
restart {
|
||||
attempts = 3
|
||||
interval = "10m"
|
||||
delay = "15s"
|
||||
mode = "fail"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -70,15 +70,15 @@
|
||||
{% for tracked_meal in meals_for_time %}
|
||||
{# 1. Create stable slugs #}
|
||||
{% set meal_time_slug = meal_time|slugify %}
|
||||
{% set meal_name_safe = tracked_meal.meal.name|slugify %}
|
||||
{% set display_meal_name = (tracked_meal.name or tracked_meal.meal.name) if (tracked_meal.name or tracked_meal.meal) else "Unnamed Meal" %}
|
||||
{% set meal_name_safe = display_meal_name|slugify %}
|
||||
|
||||
{# 2. Construct the core Unique Meal ID for non-ambiguous locating #}
|
||||
{% set unique_meal_id = meal_time_slug + '-' + meal_name_safe + '-' + loop.index|string %}
|
||||
<div class="mb-3 p-3 bg-light rounded" data-testid="meal-card-{{ unique_meal_id }}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong data-testid="meal-name-{{ unique_meal_id }}">{{ tracked_meal.meal.name
|
||||
}}</strong>
|
||||
<strong data-testid="meal-name-{{ unique_meal_id }}">{{ display_meal_name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
@@ -126,6 +126,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Display base meal foods, applying overrides #}
|
||||
{% if tracked_meal.meal %}
|
||||
{% for meal_food in tracked_meal.meal.meal_foods %}
|
||||
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in
|
||||
overrides.keys() %}
|
||||
@@ -162,6 +163,7 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# Display overridden/new foods #}
|
||||
{% for food_id, tmf in overrides.items() %}
|
||||
@@ -284,24 +286,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Third Row: Micros & Sugar -->
|
||||
<div class="col-4 mb-2">
|
||||
<div class="col-3 mb-2">
|
||||
<div class="border rounded p-1">
|
||||
<strong>{{ "%.0f"|format(day_totals.sugar) }}g</strong>
|
||||
<div class="small text-muted">Sugar</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<div class="col-3 mb-2">
|
||||
<div class="border rounded p-1">
|
||||
<strong>{{ "%.1f"|format(day_totals.fiber) }}g</strong>
|
||||
<div class="small text-muted">Fiber</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 mb-2">
|
||||
<div class="col-3 mb-2">
|
||||
<div class="border rounded p-1">
|
||||
<strong>{{ "%.0f"|format(day_totals.sodium) }}mg</strong>
|
||||
<div class="small text-muted">Sodium</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 mb-2">
|
||||
<div class="border rounded p-1">
|
||||
<strong data-testid="daily-calcium-value">{{ "%.0f"|format(day_totals.calcium) }}mg</strong>
|
||||
<div class="small text-muted">Calcium</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
15
tests/calcium_display.spec.js
Normal file
15
tests/calcium_display.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test('should display Calcium in Daily Totals section', async ({ page }) => {
|
||||
// Navigate to tracker page
|
||||
await page.goto('/tracker');
|
||||
|
||||
// Check for Daily Totals card
|
||||
const dailyTotalsCard = page.locator('.card:has-text("Daily Totals")');
|
||||
await expect(dailyTotalsCard).toBeVisible();
|
||||
|
||||
// Check for Calcium label and value using test-id
|
||||
const calciumValue = page.getByTestId('daily-calcium-value');
|
||||
await expect(calciumValue).toBeVisible();
|
||||
await expect(calciumValue).toContainText(/^[0-9]+mg$/);
|
||||
});
|
||||
@@ -98,20 +98,9 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
||||
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
|
||||
|
||||
try:
|
||||
# Find the created Meal
|
||||
created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first()
|
||||
assert created_meal is not None
|
||||
assert created_meal.name == "Test Food"
|
||||
assert created_meal.meal_type == "single_food"
|
||||
|
||||
# Find the MealFood
|
||||
meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
|
||||
assert meal_food is not None
|
||||
assert meal_food.food_id == food.id
|
||||
|
||||
# This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0
|
||||
# After the fix changing to data.get("quantity", 1.0), it will pass
|
||||
assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}"
|
||||
# Verify NO new Meal was created
|
||||
meals = query_session.query(Meal).all()
|
||||
assert len(meals) == 0
|
||||
|
||||
# Also verify TrackedDay and TrackedMeal were created
|
||||
tracked_day = query_session.query(TrackedDay).filter(
|
||||
@@ -123,8 +112,16 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
||||
|
||||
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||
assert tracked_meal is not None
|
||||
assert tracked_meal.meal_id == created_meal.id
|
||||
assert tracked_meal.meal_id is None
|
||||
assert tracked_meal.name == "Test Food"
|
||||
assert tracked_meal.meal_time == "Snack 1"
|
||||
|
||||
# Find the TrackedMealFood
|
||||
from app.database import TrackedMealFood
|
||||
tmf = query_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.food_id == food.id
|
||||
assert tmf.quantity == 50.0
|
||||
|
||||
finally:
|
||||
query_session.close()
|
||||
|
||||
@@ -140,10 +140,13 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
# Verify the tracked meal food quantity
|
||||
tracked_meal = session.query(Meal).filter(Meal.name == sample_food_100g.name).first()
|
||||
tracked_meal = session.query(TrackedMeal).filter(TrackedMeal.name == sample_food_100g.name).first()
|
||||
assert tracked_meal is not None
|
||||
meal_food = session.query(MealFood).filter(MealFood.meal_id == tracked_meal.id).first()
|
||||
assert meal_food.quantity == grams
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
tmf = session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.quantity == grams
|
||||
|
||||
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
|
||||
"""Test updating tracked meal foods with grams input"""
|
||||
|
||||
113
tests/test_tracked_meal_refactor.py
Normal file
113
tests/test_tracked_meal_refactor.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import pytest
|
||||
from app.database import Food, TrackedMeal, TrackedMealFood, calculate_tracked_meal_nutrition
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def test_calculate_tracked_meal_nutrition_no_meal_template(db_session: Session):
|
||||
"""Test nutrition calculation for a tracked meal with no parent meal template (meal_id=None)"""
|
||||
# Create a food
|
||||
food = Food(
|
||||
name="Test Food",
|
||||
serving_size=100.0,
|
||||
serving_unit="g",
|
||||
calories=100.0,
|
||||
protein=10.0,
|
||||
carbs=20.0,
|
||||
fat=5.0,
|
||||
fiber=5.0,
|
||||
sugar=10.0,
|
||||
sodium=100.0,
|
||||
calcium=50.0
|
||||
)
|
||||
db_session.add(food)
|
||||
db_session.commit()
|
||||
db_session.refresh(food)
|
||||
|
||||
# Create a tracked meal without a template
|
||||
tracked_meal = TrackedMeal(
|
||||
meal_id=None,
|
||||
meal_time="Snack",
|
||||
name="Single Food Log"
|
||||
)
|
||||
db_session.add(tracked_meal)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_meal)
|
||||
|
||||
# Add a tracked food entry to it
|
||||
tracked_food = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=food.id,
|
||||
quantity=200.0, # 2 servings
|
||||
is_override=False,
|
||||
is_deleted=False
|
||||
)
|
||||
db_session.add(tracked_food)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_food)
|
||||
|
||||
# Calculate nutrition
|
||||
nutrition = calculate_tracked_meal_nutrition(tracked_meal, db_session)
|
||||
|
||||
# Assertions
|
||||
assert nutrition['calories'] == 200.0
|
||||
assert nutrition['protein'] == 20.0
|
||||
assert nutrition['carbs'] == 40.0
|
||||
assert nutrition['fat'] == 10.0
|
||||
assert nutrition['fiber'] == 10.0
|
||||
assert nutrition['sugar'] == 20.0
|
||||
assert nutrition['sodium'] == 200.0
|
||||
assert nutrition['calcium'] == 100.0
|
||||
assert nutrition['net_carbs'] == 30.0
|
||||
assert nutrition['protein_pct'] == 40.0 # (20 * 4) / 200 = 80 / 200 = 40%
|
||||
assert nutrition['carbs_pct'] == 80.0 # (40 * 4) / 200 = 160 / 200 = 80%
|
||||
assert nutrition['fat_pct'] == 45.0 # (10 * 9) / 200 = 90 / 200 = 45%
|
||||
|
||||
def test_tracker_add_food_api_no_new_meal(client, db_session: Session):
|
||||
"""Test /tracker/add_food endpoint to ensure it doesn't create redundant Meal templates"""
|
||||
# Create a food
|
||||
food = Food(
|
||||
name="API Test Food",
|
||||
serving_size=100.0,
|
||||
serving_unit="g",
|
||||
calories=100.0,
|
||||
protein=10.0,
|
||||
carbs=20.0,
|
||||
fat=5.0
|
||||
)
|
||||
db_session.add(food)
|
||||
db_session.commit()
|
||||
db_session.refresh(food)
|
||||
|
||||
from app.database import Meal
|
||||
initial_meal_count = db_session.query(Meal).count()
|
||||
|
||||
# Call the API
|
||||
response = client.post("/tracker/add_food", json={
|
||||
"person": "Sarah",
|
||||
"date": "2025-02-24",
|
||||
"food_id": food.id,
|
||||
"quantity": 150.0,
|
||||
"meal_time": "Snack"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
# Verify NO new Meal was created
|
||||
assert db_session.query(Meal).count() == initial_meal_count
|
||||
|
||||
# Verify TrackedMeal exists with meal_id=None and correct name
|
||||
from app.database import TrackedMeal, TrackedDay
|
||||
tracked_day = db_session.query(TrackedDay).filter(TrackedDay.date == "2025-02-24").first()
|
||||
assert tracked_day is not None
|
||||
|
||||
tracked_meal = db_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||
assert tracked_meal is not None
|
||||
assert tracked_meal.meal_id is None
|
||||
assert tracked_meal.name == "API Test Food"
|
||||
|
||||
# Verify TrackedMealFood exists
|
||||
from app.database import TrackedMealFood
|
||||
tmf = db_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.food_id == food.id
|
||||
assert tmf.quantity == 150.0
|
||||
@@ -384,12 +384,13 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == sample_food.name # The meal name should be the food name
|
||||
assert tracked_meal.name == sample_food.name # The meal name should be the food name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
# Verify the food is in the tracked meal's foods
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == 100.0
|
||||
|
||||
|
||||
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
|
||||
@@ -418,11 +419,12 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == sample_food.name
|
||||
assert tracked_meal.name == sample_food.name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == 150.0
|
||||
|
||||
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
|
||||
"""
|
||||
@@ -464,12 +466,13 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == food.name
|
||||
assert tracked_meal.name == food.name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
# Verify the food is in the tracked meal's foods and quantity is in servings
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
|
||||
# Verify the food is in the tracked meal's foods
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == grams_to_add
|
||||
|
||||
# Verify nutrition calculation
|
||||
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)
|
||||
|
||||
32
tests/tracked_meal_refactor.spec.js
Normal file
32
tests/tracked_meal_refactor.spec.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test('add single food to tracker and verify it is not in meals page', async ({ page }) => {
|
||||
await page.goto('/tracker');
|
||||
|
||||
// Add single food to breakfast
|
||||
await page.locator('[data-testid="add-food-breakfast"]').click();
|
||||
// Select a food (Verification Beans)
|
||||
await page.locator('#addSingleFoodModal select[name="food_id"]').selectOption({ label: 'Verification Beans' });
|
||||
await page.locator('#addSingleFoodModal input[name="quantity"]').fill('200');
|
||||
await page.getByRole('button', { name: 'Add Food', exact: true }).click();
|
||||
|
||||
// Verify it appears in the tracker
|
||||
// The name should be just the food name
|
||||
const mealNameLocator = page.locator('[data-testid^="meal-name-breakfast-verification-beans"]');
|
||||
await expect(mealNameLocator).toBeVisible();
|
||||
await expect(mealNameLocator).toHaveText('Verification Beans');
|
||||
|
||||
// Verify it contains the food with correct quantity
|
||||
const foodRowLocator = page.locator('[data-testid^="food-row-breakfast-verification-beans"][data-testid$="verification-beans"]');
|
||||
await expect(foodRowLocator).toBeVisible();
|
||||
await expect(foodRowLocator).toContainText('Verification Beans');
|
||||
await expect(foodRowLocator).toContainText('200.0 g');
|
||||
|
||||
// Navigate to Meals page
|
||||
await page.goto('/meals');
|
||||
|
||||
// Verify 'Verification Beans' is NOT in the meals list as a meal name
|
||||
// It might be in the ingredients dropdown, but shouldn't be a <strong> heading in a card
|
||||
const mealCardHeading = page.locator('.card-title:has-text("Verification Beans")');
|
||||
await expect(mealCardHeading).not.toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user