# kcal – the personal food nutrition journal [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) [![CI Status](https://github.com/kcal-app/kcal/actions/workflows/ci.yml/badge.svg)](https://github.com/kcal-app/kcal/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/kcal-app/kcal/badge.svg?branch=main)](https://coveralls.io/github/kcal-app/kcal?branch=main) Track nutritional information about foods and recipes, set goals, and record a food journal to help along the way. Kcal is a *personal* system that focuses on direct control of inputs (as opposed to unwieldy user generated datasets) and a minimal, easy to use recipe presentation for preparing meals. ## Screenshots ![kcal mobile screenshot](screenshots/mobile.png) ![kcal desktop screenshot](screenshots/desktop.png) ## Functionality Kcal's primary functionality includes three main content categories -- journal, recipes, and foods -- and two per-user configurations -- goals and meals. Users can customize meals (up to eight per day) for meal planning, set multiple goals for tracking calories and macros (fat, carbohydrates, protein), enter food data with calories and macros, create recipes based on foods and other recipes, and record foods and recipes in a journal tracked against goals. ### Foods Foods are shared between all users. Foods are the basis for recipes and calorie and macro calculations throughout the app. They can be added directly to journal entries and included in recipes that roll-up nutritional data per serving. Food servings can be recorded using U.S. measure units (teaspoon, tablespoon, cup) and use a weight basis of grams. These units and weights are commonly used in the United States and required by law for most packaged foods. Kcal tries to make data entry as easy as possible by organization field order and units to match nutritional label configurations. Food data does not (currently) make use of any API or service for retrieving information about food. While this may change in the future, there are a couple of reason this feature has been left out of kcal: - There is a very large volume of data in most food databases such that organizing, categorizing, and searching for the right food can be daunting. - Food databases that allow and include input from large groups of users can be inaccurate and counter-productive for users with calorie and/or macro goals. - Kcal's self-hostable nature is in part for privacy-conscious users so limiting interactions with third-party services is desirable (though adding APIs as an optional enhancement is not a major concern here). Foods are taggable and tags can be used to filter and search for foods quickly. ![kcal foods edit and show screenshot](screenshots/foods.png) ### Recipes Recipes are shared between all users. They (currently) cannot be shared externally, but a feature enhancement to support optional, per-recipe public link creation is planned. Configurable recipe metadata includes servings, weight, volume, timing, image, description, ingredients, and steps. The recipe view is meant to be as uncluttered as possible, to support browser "reader mode", and to still provide nutritional metadata at a glance. Recipes are taggable and tags can be used to filter and search for recipes quickly. ![kcal foods edit and show screenshot](screenshots/recipes.png) ### Journal entries Journal entries are private. Each user has and can only see their own entries. Planning and tracking calories and macros is the primary focus of journal entries. The journal section aggregates this information by day and by meal. Recipes and foods can be added to journal entries in various quantities and configurations (depending on the associated serving data). Manual entries can also be used to record data for foods and recipes *not* in the kcal's database. Journal entries are not editable. The calorie and macro information for each entry is recorded with the entry. This allows for recoding and maintaining journal entry data even as foods and recipes are updated. ![kcal journal entries edit and show screenshot](screenshots/journal-entries.png) ### Goals Goals are private. Each user can create and see only their own goals. Goals reflect daily calories and macros and are therefore closely related to journal entries. Goals can be configured as "default" for specific days of the week but can also be overridden for individual days in the journal section. There is no limit to the number of goals a user can create. ![kcal goals edit and show screenshot](screenshots/goals.png) ### Meals Meals are private. Each user can rename, arrange, and enable or disabled their own meals. Meals are used to aggregate data within a day in the journal section and can be used for both meal planning and goal tracking. There is a pre-configured maximum of eight meals for each user. ![kcal meals edit and show screenshot](screenshots/meals.png) ## Use cases Kcal's primary focus is tracking nutrition (recipe management is the most important secondary focus), so it should be a useful tool for anyone looking to implement a specific diet. Below are some (very) broad diet types and information about how kcal can be helpful. Kcal intentionally **does not** provide any specific dieting guidance or recommend any particular diet or type of diet. Individual users are expected to research and make their own plans and goals -- kcal is here to help record and organize the data. ### Hypocaloric diets Hypocaloric diets use [calorie restriction](https://en.wikipedia.org/wiki/Calorie_restriction) for weight loss or other dietary management. Kcal users can create low calorie (relative to personal regular calorie) intake goals and use the detailed nutritional data of food and recipes and to plan meals and record caloric intake using journal entries. ### Hypercaloric diets Hypercaloric diets are used for weight gain and can be especially useful for building muscle in weight training. Kcal users can create calorie goals that exceed expected calorie burn on a day-to-day basis. For the weight training example users can create a goal for training days and separate, lower goal for rest days. The goals can be automated based on the day of the week and journal entries can be used to ensure that a proper macro balance is maintained. More meals may also be helpful on a hypercaloric diet and kcal supports up to eight meals per day. ### Plant based diets (vegetarian, vegan, etc.) Diets that exclude animal-based products may require special attention to ensure a proper balance of nutrients (particularly when transitioning). Kcal's goals and food nutritional data can help to make sure that appropriate fat, carbohydrate, and protein needs are met. ### Low-X diets Any diet that focuses on lowering a particular nutrient can be trackable with kcal. In addition to the macronutrients (fat, carbohydrates, and protein) foods support cholesterol and sodium data as well. Support for other common nutrients like saturated fats, trans fats, fiber, sugar, etc. may also be available in future iterations. ## Deployment Docker is the recommended deployment method. See [kcal-app/kcal-docker](https://github.com/kcal-app/kcal-docker). General requirements for any deployment are: - PHP 8.x with Composer 2.x and PHP extensions: `bcmath`, `curl`, `gd`, `intl`, `mbstring`, `xml`, `zip`. - Web server/proxy (Apache, nginx, etc.) - Database (MySQL/MariaDB, PostgreSQL, etc.) Optional but useful additions are: - [Search driver](#search-mag) (Algolia, Elasticsearch, and database supported) - Redis - [Media Storage](#media-storage) (local or AWS S3 supported) ### Docker There is a [`Dockerfile`](Dockerfile) and automated build process to create builds at [kcalapp/kcal](https://hub.docker.com/repository/docker/kcalapp/kcal) on Docker Hub. See the [kcal-app/kcal-docker](https://github.com/kcal-app/kcal-docker) repository for a Docker Compose based template and instructions. ### Heroku [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) The default username and password for a Heroku deployment is `kcal`/`kcal`. #### Using Heroku CLI For a manual deploy using Heroku CLI, execute the following after initial deployment: heroku run php artisan migrate heroku run php artisan user:add heroku config:set APP_KEY=$(php artisan --no-ansi key:generate --show) #### Media storage Heroku uses an ephemeral disk. In order to maintain recipe and/or user images between app restarts AWS can be used. See [Media Storage - AWS S3](#aws-s3) for additional guidance. #### Search drivers See the [Search](#search-mag) section for information about supported drivers. Additional environment variable configuration is necessary when using any search driver other than the default ("null"). #### Redis Add-on The [Heroku Redis](https://elements.heroku.com/addons/heroku-redis) add-on can be added to the app and will work without any configuration changes. It is left out of the default build only because it takes a very long time to provision. ### Manual This deployment process has been tested with an Ubuntu 20.04 LTS instance with 2GB of memory which should be enough to host the app for a few regular users. The memory is primarily needed for Elasticsearch -- See the [Search](#search-mag) section for other options if lower memory support is needed. 1. Add [PHP 8.x repository](https://launchpad.net/~ondrej/+archive/ubuntu/php). sudo apt-get install software-properties-common sudo add-apt-repository ppa:ondrej/php 1. Add [Elasticsearch 7.x repository](https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html). wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list 1. Update available packages. sudo apt-get update 1. Install dependencies. sudo apt-get install elasticsearch mysql-server-8.0 nginx-full php8.2 php8.2-bcmath php8.2-cli php8.2-curl php8.2-gd php8.2-intl php8.2-mbstring php8.2-mysql php8.2-redis php8.2-xml php8.2-zip redis php8.2-fpm 1. Start Elasticsearch and configure to run at start up. sudo systemctl start elasticsearch sudo systemctl enable elasticsearch 1. Install Composer. :rotating_light: This command runs code from a remote location as root. See [Download Composer](https://getcomposer.org/download/) for alternative install options. curl -s https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin/ --filename=composer 1. Clone the app repository. cd /var/www sudo mkdir kcal sudo chown $USER:`id -gn $USER` kcal cd kcal git clone https://github.com/kcal-app/kcal.git . 1. Configure nginx to serve the app public files. sudo vim /etc/nginx/conf.d/kcal.conf sudo service nginx restart Example config: server { listen 80; server_name kcal.example.com; root /var/www/kcal/public; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.(?!well-known).* { deny all; } } 1. Create database user. sudo mysql -u root CREATE DATABASE `kcal`; CREATE USER 'kcal'@'localhost' IDENTIFIED BY RANDOM PASSWORD; GRANT ALL ON `kcal`.* TO 'kcal'@'localhost'; FLUSH PRIVILEGES; :lock: Save the generated password output by the `CREATE USER` statement. 1. Install dependencies and generate an app key to use in the next step. composer install --optimize-autoloader --no-dev php artisan --no-ansi key:generate --show 1. Copy environment config file and adjust as desired. cp .env.example .env At a minimum: - Set `APP_KEY` to the value generated in the previous step. - Set `APP_URL` to match the host configured in nginx configuration. - Set the `DATABASE_` values to the configured credentials. 1. Run initial app installation/bootstrap commands. php artisan migrate php artisan elastic:migrate php artisan config:cache php artisan route:cache php artisan view:cache php artisan user:add --admin 1. Allow web server to access required directories. sudo chown -R $USER:www-data {storage,public} sudo chmod g+s {storage,public} 1. Visit the `APP_URL` and log in! ## Configuration ### Media Storage Recipes and users can have associated media (images) that by default are stored on a local disk under the path `{app}/public/media`. If a local disk solution is not feasible, an AWS S3 bucket can be used instead. #### AWS S3 Use the general guidance below to create an AWS S3 bucket and IAM user for media storage in AWS S3. 1. Create a bucket that allows objects to be configured with public access. 1. Create an IAM user with access to the bucket. Use this example policy to grant necessary permissions to a specific bucket: { "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "s3:GetBucketPublicAccessBlock", "s3:GetBucketPolicyStatus", "s3:GetAccountPublicAccessBlock", "s3:ListAllMyBuckets", "s3:GetBucketAcl", "s3:GetBucketLocation" ], "Resource": "*" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::REPLACE_WITH_S3_BUCKET_NAME" }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": ["s3:*Object", "s3:*ObjectAcl*"], "Resource": "arn:aws:s3:::REPLACE_WITH_S3_BUCKET_NAME/*" } ] } 1. Set necessary environment variables (via `.env` or some other mechanism). MEDIA_DISK=s3-public AWS_ACCESS_KEY_ID=REPLACE_WITH_IAM_KEY AWS_SECRET_ACCESS_KEY=REPLACE_WITH_IAM_SECRET AWS_DEFAULT_REGION=REPLACE_WITH_S3_BUCKET_NAME AWS_BUCKET=REPLACE_WITH_S3_BUCKET_REGION ### Search :mag: The "ingredient" (food or recipe) search for journal entries and recipe ingredients supports three different backends using the `SCOUT_DRIVER` environment variable. In all cases, always ensure that the `SCOUT_DRIVER` environment variable is only set once in kcal's `.env` file. Currently, the food and recipe *list* searches do not take advantage of these search drivers. Support for those searches will be added if the Laravel JSON:API adds support for Scout (see: laravel-json-api/laravel#32). ### Algolia (`algolia`) 1. [Create and/or log in](https://www.algolia.com/users/sign_in) to an Algolia account. 1. Create an application for kcal. 1. Navigate to the application's "API Keys" section. 1. Using the **Application ID** and **Admin API Key** values, update kcal's `.env` file: SCOUT_DRIVER=algolia ALGOLIA_APP_ID= ALGOLIA_SECRET= ### ElasticSearch (`elastic`) 1. Determine the ElasticSearch service host and port. 1. Update kcal's `.env` file. SCOUT_DRIVER=elastic ELASTIC_HOST= ELASTIC_PORT= Note: The `ELASTIC_PORT` variable is a convenience option specifically for Docker Compose configurations and is not strictly required. 1. Run Elastic's migrations. php artisan elastic:migrate ### Fallback (`null`) The fallback driver is a simple `WHERE ... LIKE` clause search on a couple of key fields. Results will not be ordered by relevance, and some fields will not be searched (e.g. the tags fields). Using one of the other options is highly recommended. Set `SCOUT_DRIVER=null` in kcal's `.env` file to use the fallback driver. ## Development ### Laravel Sail #### Prerequisites - [Composer](https://getcomposer.org/download/) - [Docker](https://docs.docker.com/get-docker/) - [Docker compose](https://docs.docker.com/compose/install/) #### Steps 1. Clone the repository. git clone https://github.com/kcal-app/kcal.git cd kcal 1. Install development dependencies. composer install 1. Create a local `.env` file. cp .env.example .env 1. Generate an app key. php artisan key:generate Verify that the `APP_KEY` variable has been set in `.env`. If has not, run `php artisan key:generate --show` and copy the key and append it to the `APP_KEY=` line manually. 1. Run it! :sailboat: vendor/bin/sail up 1. (On first run) Run migrations. vendor/bin/sail artisan migrate vendor/bin/sail artisan elastic:migrate 1. (On first run) Seed the database. vendor/bin/sail artisan db:seed The default username and password is `kcal` / `kcal`. Navigate to [http://127.0.0.1:8080](http://127.0.0.1:8080) to log in! Create a `docker-compose.override.yml` file to override any of the default settings provided for this environment. ### Custom console commands #### `dev:cache-clear` Executes the various cache clearing artisan commands: - `cache:clear` - `config:clear` - `route:clear` - `view:clear` #### `dev:reset` Resets and seeds the database by executing the following artisan commands: - `db:wipe` - `migrate` - `db:seed` ### Testing Ensure that Sail is running (primarily to provide ElasticSearch): vendor/bin/sail up -d Execute tests. vendor/bin/sail artisan dev:cache-clear vendor/bin/sail artisan test --parallel --recreate-databases