12 Commits

Author SHA1 Message Date
7ff345d3a2 Do not update pip 2025-08-14 04:07:15 +02:00
Stef
944ba17b31 Merge pull request #4 from StefBuwalda/development
Removed accidental packages from requirements.txt
2025-08-14 04:05:24 +02:00
8820fc07d7 Removed accidental packages from requirements.txt 2025-08-14 04:04:48 +02:00
Stef
072dd2c651 Merge pull request #3 from StefBuwalda/development
Update requirements.txt
2025-08-14 03:46:54 +02:00
8acb8453ea Update requirements.txt 2025-08-14 03:46:26 +02:00
Stef
fb160d364a Merge pull request #2 from StefBuwalda/development
Hopefully add caching to docker build action
2025-08-14 03:42:12 +02:00
fdf264b9c5 Update docker-ghcr.yml 2025-08-14 03:41:34 +02:00
91595ccc11 Caching pip downloads (hopefully) 2025-08-14 03:39:39 +02:00
6c01c6a923 Update Dockerfile 2025-08-14 03:34:31 +02:00
Stef
5a9e1f77c7 Merge pull request #1 from StefBuwalda/development
Adds a new daily log page made with AI assistance.
2025-08-14 03:18:58 +02:00
2454bc61cb Moved the bar width calculation from jinja to the dashboard values generation 2025-08-14 03:15:02 +02:00
85297daaaf Add new daily log dashboard and macro summary
Introduces a new route and template for an enhanced daily log dashboard with macro nutrient summary. Refactors FoodItem model to add type annotations and a macros() method, and updates form handling to default missing values to zero. Also adds a navigation link to the new dashboard in the base template.
2025-08-14 02:49:06 +02:00
9 changed files with 176 additions and 30 deletions

View File

@@ -14,9 +14,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Checkout code
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
# Log in to GHCR
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3.5.0 uses: docker/login-action@v3.5.0
with: with:
@@ -24,13 +26,22 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
# Set up Docker Buildx (needed for ARM64 cross-build)
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1 uses: docker/setup-buildx-action@v3.11.1
# Build and push Docker image with caching
- name: Build and push ARM64 image - name: Build and push ARM64 image
uses: docker/build-push-action@v6.18.0 uses: docker/build-push-action@v6.18.0
with: with:
context: . context: .
push: true push: true
platforms: linux/arm64 platforms: linux/arm64
tags: ghcr.io/stefbuwalda/cal_counter:arm64,ghcr.io/stefbuwalda/cal_counter:latest tags: |
ghcr.io/stefbuwalda/cal_counter:arm64
ghcr.io/stefbuwalda/cal_counter:latest
cache-from: type=registry,ref=ghcr.io/stefbuwalda/cal_counter:cache
cache-to: type=registry,ref=ghcr.io/stefbuwalda/cal_counter:cache,mode=max
build-args: |
PIP_NO_CACHE_DIR=1

View File

@@ -2,8 +2,13 @@ FROM python:3.12-slim
# Everything will be done in /app (Not in the main OS Image) # Everything will be done in /app (Not in the main OS Image)
WORKDIR /app WORKDIR /app
COPY . .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN chmod +x ./entrypoint.sh RUN chmod +x ./entrypoint.sh
ENV FLASK_APP=app.py ENV FLASK_APP=app.py

1
app.py
View File

@@ -5,7 +5,6 @@ from flask import (
) )
from flask_login import ( from flask_login import (
login_required, login_required,
logout_user,
current_user, current_user,
) )
from models import User from models import User

View File

View File

@@ -25,6 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('user.daily_log') }}">Daily Log</a> <a class="nav-link" href="{{ url_for('user.daily_log') }}">Daily Log</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.daily_log2') }}">Daily Log (new)</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('user.dashboard') }}">Dashboard</a> <a class="nav-link" href="{{ url_for('user.dashboard') }}">Dashboard</a>
</li> </li>

View File

@@ -13,6 +13,8 @@ from forms import FoodItemForm
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from application.utils import login_required from application.utils import login_required
from typing import cast
from numpy import array
user_bp = Blueprint( user_bp = Blueprint(
"user", "user",
@@ -21,6 +23,57 @@ user_bp = Blueprint(
) )
def macro_arr_to_json(data: list[float]):
assert len(data) == 4
cal = data[0]
pro = data[3]
car = data[2]
fat = data[1]
macros = [
{
"name": "Calories",
"current": cal,
"target": 2000,
"bar_width": 100 - abs(cal / 20 - 100),
"bar_width_overflow": max(0, cal / 20 - 100),
"unit": " kcal",
"color": "bg-calories",
"overflow_color": "bg-calories-dark",
},
{
"name": "Protein",
"current": pro,
"target": 150,
"bar_width": 100 - abs(pro / 1.5 - 100),
"bar_width_overflow": max(0, pro / 1.5 - 100),
"unit": "g",
"color": "bg-protein",
"overflow_color": "bg-protein-dark",
},
{
"name": "Carbs",
"current": car,
"target": 250,
"bar_width": 100 - abs(car / 2.5 - 100),
"bar_width_overflow": max(0, car / 2.5 - 100),
"unit": "g",
"color": "bg-carbs",
"overflow_color": "bg-carbs-dark",
},
{
"name": "Fat",
"current": fat,
"target": 70,
"bar_width": 100 - abs(fat / 0.7 - 100),
"bar_width_overflow": max(0, fat / 0.7 - 100),
"unit": "g",
"color": "bg-fat",
"overflow_color": "bg-fat-dark",
},
]
return macros
user_bp.before_request(login_required) user_bp.before_request(login_required)
@@ -100,6 +153,18 @@ def daily_log(offset: int = 0):
) )
@user_bp.route("/daily_log2", methods=["GET"])
def daily_log2():
today = datetime.now(timezone.utc).date()
logs_today = current_user.food_logs.filter_by(date_=today).all()
macros = array((0.0, 0.0, 0.0, 0.0))
for log in logs_today:
item = cast(FoodItem, log.food_item)
macros += array(item.macros()) / 100 * log.amount
macros = macro_arr_to_json(macros.tolist())
return render_template("daily_log2.html", macros=macros, logs=logs_today)
@user_bp.route("/remove_log/<int:id>", methods=["POST"]) @user_bp.route("/remove_log/<int:id>", methods=["POST"])
def remove_log(id: int): def remove_log(id: int):
log = db.session.get(FoodLog, id) log = db.session.get(FoodLog, id)

View File

@@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% block title %}Daily Calorie Dashboard{% endblock %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/macros.css') }}">
<div class="container my-4">
<h2 class="mb-3">Daily Calorie Dashboard</h2>
<!-- Macro Summary -->
<div class="card p-3 mb-3">
<h5>Macros</h5>
{% for macro in macros %}
<div class="mb-2">
<span class="macro-text">{{ macro.name }}: {{ macro.current }} / {{ macro.target }}</span>
<div class="progress rounded" style="height: 24px;">
<div class="progress-bar bg-danger macro-bar" role="progressbar"
style="width: {{ macro.bar_width_overflow }}%">
{{ macro.current - macro.target }}{{ macro.unit }}
</div>
<div class="progress-bar bg-success macro-bar" role="progressbar" style="width: {{ macro.bar_width }}%">
{{ macro.current }}{{ macro.unit }}
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Items List -->
<div class="card p-3">
<h5>Items Eaten Today</h5>
<div class="list-group list-group-flush">
{% for log in logs %}
<div class="list-group-item item-row d-flex justify-content-between align-items-center bg-dark text-light">
<span>({{ log.amount }}g) {{ log.food_item.name }}</span>
<span>{{ log.food_item.energy_100 }} kcal</span>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Bottom Navigation Buttons -->
<div class="container-fluid fixed-bottom py-2">
<div class="d-flex p-3">
<!-- Left Button -->
<a href="" class="btn btn-outline-light flex-fill me-2 rounded-pill">
Previous
</a>
<!-- Center Button (highlighted) -->
<a href="{{ url_for('add_meal.step1', meal_type=0) }}"
class="btn btn-success flex-fill mx-2 fw-bold rounded-pill">
Add Item
</a>
<!-- Right Button -->
<a href="" class="btn btn-outline-light flex-fill ms-2 rounded-pill">
Next
</a>
</div>
</div>
{% endblock %}

View File

@@ -49,17 +49,18 @@ class Unit(db.Model):
class FoodItem(db.Model): class FoodItem(db.Model):
__tablename__ = "food_item" __tablename__ = "food_item"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
barcode = db.Column(db.String) barcode = db.Column(db.String)
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
name = db.Column(db.String(150), nullable=False) name = db.Column(db.String(150), nullable=False)
energy_100 = db.Column(db.Float, nullable=False) energy_100: float = db.Column(db.Float, nullable=False)
protein_100 = db.Column(db.Float, nullable=False) protein_100: float = db.Column(db.Float, nullable=False)
carbs_100 = db.Column(db.Float, nullable=False) carbs_100: float = db.Column(db.Float, nullable=False)
sugar_100 = db.Column(db.Float) sugar_100: Optional[float] = db.Column(db.Float)
fat_100 = db.Column(db.Float, nullable=False) fat_100: float = db.Column(db.Float, nullable=False)
saturated_fat_100 = db.Column(db.Float) saturated_fat_100: Optional[float] = db.Column(db.Float)
food_logs = db.relationship( food_logs = db.relationship(
"FoodLog", "FoodLog",
@@ -99,26 +100,20 @@ class FoodItem(db.Model):
def updateFromForm(self, form: FoodItemForm): def updateFromForm(self, form: FoodItemForm):
self.name = form.name.data self.name = form.name.data
self.energy_100 = form.energy.data self.energy_100 = form.energy.data or 0
self.protein_100 = form.protein.data self.protein_100 = form.protein.data or 0
self.carbs_100 = form.carbs.data self.carbs_100 = form.carbs.data or 0
self.sugar_100 = form.sugar.data self.sugar_100 = form.sugar.data
self.fat_100 = form.fat.data self.fat_100 = form.fat.data or 0
self.saturated_fat_100 = form.saturated_fat.data self.saturated_fat_100 = form.saturated_fat.data
def to_dict(self): def macros(self) -> tuple[float, float, float, float]:
return { return (
"id": self.id, self.energy_100,
"barcode": self.barcode, self.fat_100,
"name": self.name, self.carbs_100,
"owner_id": self.owner_id, self.protein_100,
"energy_100": self.energy_100, )
"protein_100": self.protein_100,
"carbs_100": self.carbs_100,
"sugar_100": self.sugar_100,
"fat_100": self.fat_100,
"saturated_fat_100": self.saturated_fat_100,
}
class FoodLog(db.Model): class FoodLog(db.Model):

View File

@@ -1,4 +1,4 @@
alembic==1.16.1 alembic==1.16.4
blinker==1.9.0 blinker==1.9.0
click==8.2.1 click==8.2.1
colorama==0.4.6 colorama==0.4.6
@@ -7,12 +7,13 @@ Flask-Login==0.6.3
Flask-Migrate==4.1.0 Flask-Migrate==4.1.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.2 Flask-WTF==1.2.2
greenlet==3.2.2 greenlet==3.2.4
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.6 Jinja2==3.1.6
Mako==1.3.10 Mako==1.3.10
MarkupSafe==3.0.2 MarkupSafe==3.0.2
SQLAlchemy==2.0.41 numpy==2.3.2
typing_extensions==4.13.2 SQLAlchemy==2.0.43
typing_extensions==4.14.1
Werkzeug==3.1.3 Werkzeug==3.1.3
WTForms==3.2.1 WTForms==3.2.1