diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 31d8f3427..b83fbf19c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -48,7 +48,7 @@ ], // Use 'onCreateCommand' to run commands at the end of container creation. // Use 'postCreateCommand' to run commands after the container is created. - "onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules && task setup", + "onCreateCommand": "sudo chown -R vscode:vscode /workspaces/mealie/frontend/node_modules /home/vscode/commandhistory && task setup", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { diff --git a/.dockerignore b/.dockerignore index 49ef68257..602e1dd8e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,7 @@ .idea .vscode -__pycache__/ +**/__pycache__/ *.py[cod] *$py.class *.so @@ -25,10 +25,10 @@ venv */node_modules */dist +/dist/ */data/db */mealie/test */mealie/.temp - -model.crfmodel +/mealie/frontend/ crowdin.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 48d020811..df3e5b3a2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,8 +3,15 @@ on: workflow_call: jobs: + build-package: + name: "Build Python package" + uses: ./.github/workflows/partial-package.yml + with: + tag: e2e + test: timeout-minutes: 60 + needs: build-package runs-on: ubuntu-latest defaults: run: @@ -18,11 +25,18 @@ jobs: cache-dependency-path: ./tests/e2e/yarn.lock - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Retrieve Python package + uses: actions/download-artifact@v4 + with: + name: backend-dist + path: dist - name: Build Image uses: docker/build-push-action@v5 with: file: ./docker/Dockerfile context: . + build-contexts: | + packages=dist push: false load: true tags: mealie:e2e diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index fdf1c00c2..e2558c4bc 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,7 +21,7 @@ jobs: uses: ./.github/workflows/partial-backend.yml frontend-tests: - name: "Frontend and End-to-End Tests" + name: "Frontend Tests" uses: ./.github/workflows/partial-frontend.yml build-release: diff --git a/.github/workflows/partial-backend.yml b/.github/workflows/partial-backend.yml index b0772d181..89d809d16 100644 --- a/.github/workflows/partial-backend.yml +++ b/.github/workflows/partial-backend.yml @@ -1,4 +1,4 @@ -name: Backend Test/Lint +name: Backend Lint and Test on: workflow_call: diff --git a/.github/workflows/partial-builder.yml b/.github/workflows/partial-builder.yml index c6362ba3b..573325da1 100644 --- a/.github/workflows/partial-builder.yml +++ b/.github/workflows/partial-builder.yml @@ -16,7 +16,14 @@ on: required: true jobs: + build-package: + name: "Build Python package" + uses: ./.github/workflows/partial-package.yml + with: + tag: ${{ inputs.tag }} + publish: + needs: build-package runs-on: ubuntu-latest steps: - name: Checkout repository @@ -35,18 +42,22 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Override __init__.py - run: | - echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py - - uses: depot/setup-action@v1 + - name: Retrieve Python package + uses: actions/download-artifact@v4 + with: + name: backend-dist + path: dist + - name: Build and push Docker image, via Depot.dev uses: depot/build-push-action@v1 with: project: srzjb6mhzm file: ./docker/Dockerfile context: . + build-contexts: | + packages=dist platforms: linux/amd64,linux/arm64 push: true tags: | diff --git a/.github/workflows/partial-frontend.yml b/.github/workflows/partial-frontend.yml index bbebe4cca..00f8a2673 100644 --- a/.github/workflows/partial-frontend.yml +++ b/.github/workflows/partial-frontend.yml @@ -1,4 +1,4 @@ -name: Frontend Build/Lin +name: Frontend Lint and Test on: workflow_call: @@ -41,37 +41,3 @@ jobs: - name: Run tests ๐Ÿงช run: yarn test:ci working-directory: "frontend" - - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout ๐Ÿ›Ž - uses: actions/checkout@v4 - - - name: Setup node env ๐Ÿ— - uses: actions/setup-node@v4.0.0 - with: - node-version: 16 - check-latest: true - - - name: Get yarn cache directory path ๐Ÿ›  - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - - name: Cache node_modules ๐Ÿ“ฆ - uses: actions/cache@v4 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install dependencies ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป - run: yarn - working-directory: "frontend" - - - name: Run Build ๐Ÿšš - run: yarn build - working-directory: "frontend" diff --git a/.github/workflows/partial-package.yml b/.github/workflows/partial-package.yml new file mode 100644 index 000000000..1ee258562 --- /dev/null +++ b/.github/workflows/partial-package.yml @@ -0,0 +1,102 @@ +name: Package build + +on: + workflow_call: + inputs: + tag: + required: true + type: string + +jobs: + build-frontend: + name: Build frontend + runs-on: ubuntu-latest + + steps: + - name: Checkout ๐Ÿ›Ž + uses: actions/checkout@v4 + + - name: Setup node env ๐Ÿ— + uses: actions/setup-node@v4.0.0 + with: + node-version: 16 + check-latest: true + + - name: Get yarn cache directory path ๐Ÿ›  + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - name: Cache node_modules ๐Ÿ“ฆ + uses: actions/cache@v4 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป + run: yarn + working-directory: "frontend" + + - name: Run Build ๐Ÿšš + run: yarn generate + working-directory: "frontend" + + - name: Archive built frontend + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: frontend/dist + retention-days: 5 + + build-package: + name: Build Python package + needs: build-frontend + runs-on: ubuntu-latest + + steps: + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + plugins: | + poetry-plugin-export + + - name: Retrieve built frontend + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: mealie/frontend + + - name: Override __init__.py + run: | + echo "__version__ = \"${{ inputs.tag }}\"" > ./mealie/__init__.py + + - name: Build package and requirements.txt + env: + SKIP_PACKAGE_DEPS: true + run: | + task py:package + + - name: Archive built package + uses: actions/upload-artifact@v4 + with: + name: backend-dist + path: dist + retention-days: 5 diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index ad2fa13e3..1cddb2d52 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -19,7 +19,7 @@ jobs: uses: ./.github/workflows/partial-backend.yml frontend-tests: - name: "Frontend and End-to-End Tests" + name: "Frontend Tests" uses: ./.github/workflows/partial-frontend.yml container-scanning: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fbb398cc..55b0ec5d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: uses: ./.github/workflows/partial-backend.yml frontend-tests: - name: "Frontend and End-to-End Tests" + name: "Frontend Tests" uses: ./.github/workflows/partial-frontend.yml build-release: diff --git a/.gitignore b/.gitignore index f853ecb97..cd0725b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ pnpm-debug.log* env/ build/ develop-eggs/ - +/dist/ downloads/ eggs/ .eggs/ @@ -66,6 +66,9 @@ wheels/ .installed.cfg *.egg +# frontend copied into Python module for packaging purposes +/mealie/frontend/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -154,10 +157,8 @@ dev/data/backups/dev_sample_data*.zip dev/data/recipes/* dev/scripts/output/app_routes.py dev/scripts/output/javascriptAPI/* -mealie/services/scraper/ingredient_nlp/model.crfmodel dev/code-generation/generated/openapi.json dev/code-generation/generated/test_routes.py -mealie/services/parser_services/crfpp/model.crfmodel lcov.info dev/code-generation/openapi.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acbc1c120..fb34ac013 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: exclude: ^tests/data/ - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.9.0 + rev: v0.11.0 hooks: - id: ruff - id: ruff-format diff --git a/.vscode/settings.json b/.vscode/settings.json index e4bfdeb9d..b842cd45e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,5 +60,9 @@ }, "[vue]": { "editor.formatOnSave": false + }, + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" } } diff --git a/Taskfile.yml b/Taskfile.yml index 70f6f23dd..c42cb76c7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -41,35 +41,31 @@ tasks: setup:ui: desc: setup frontend dependencies dir: frontend + run: once cmds: - yarn install + sources: + - package.json + - yarn.lock + generates: + - node_modules/** setup:py: desc: setup python dependencies + run: once cmds: - poetry install --with main,dev,postgres - poetry run pre-commit install - - setup:model: - desc: setup nlp model - vars: - MODEL_URL: https://github.com/mealie-recipes/nlp-model/releases/download/v1.0.0/model.crfmodel - OUTPUT: ./mealie/services/parser_services/crfpp/model.crfmodel sources: - # using pyproject.toml as the dependency since this should only ever need to run once - # during setup. There is perhaps a better way to do this. - - ./pyproject.toml - generates: - - ./mealie/services/parser_services/crfpp/model.crfmodel - cmds: - - curl -L0 {{ .MODEL_URL }} --output {{ .OUTPUT }} + - poetry.lock + - pyproject.toml + - .pre-commit-config.yaml setup: desc: setup all dependencies deps: - setup:ui - setup:py - - setup:model dev:generate: desc: run code generators @@ -131,6 +127,63 @@ tasks: - poetry run coverage html - open htmlcov/index.html + py:package:copy-frontend: + desc: copy the frontend files into the Python package + internal: true + deps: + - ui:generate + cmds: + - rm -rf mealie/frontend + - cp -a frontend/dist mealie/frontend + sources: + - frontend/dist/** + generates: + - mealie/frontend/** + + py:package:generate-requirements: + desc: Generate requirements file to pin all packages, effectively a "pip freeze" before installation begins + internal: true + cmds: + - poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt + # Include mealie in the requirements, hashing the package that was just built to ensure it's the one installed + - echo "mealie[pgsql]=={{.MEALIE_VERSION}} \\" >> dist/requirements.txt + - poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt + - echo " \\" >> dist/requirements.txt + - poetry run pip hash dist/mealie-{{.MEALIE_VERSION}}.tar.gz | tail -n1 >> dist/requirements.txt + vars: + MEALIE_VERSION: + sh: poetry version --short + sources: + - poetry.lock + - pyproject.toml + - dist/mealie-*.whl + - dist/mealie-*.tar.gz + generates: + - dist/requirements.txt + + py:package:deps-parallel: + desc: Run py:package dependencies in parallel + internal: true + deps: + - setup:py + - py:package:copy-frontend + + py:package:deps: + desc: Dependencies of py:package, skippable by setting SKIP_PACKAGE_DEPS=true + internal: true + cmds: + - task: py:package:deps-parallel + status: + - '{{ .SKIP_PACKAGE_DEPS | default "false"}}' + + py:package: + desc: builds Python packages (sdist and wheel) in top-level dist directory + deps: + - py:package:deps + cmds: + - poetry build -n --output=dist + - task: py:package:generate-requirements + py: desc: runs the backend server cmds: @@ -160,6 +213,14 @@ tasks: cmds: - yarn build + ui:generate: + desc: generates a static version of the frontend in frontend/dist + dir: frontend + deps: + - setup:ui + cmds: + - yarn generate + ui:lint: desc: runs the frontend linter dir: frontend @@ -184,6 +245,16 @@ tasks: cmds: - yarn run dev + docker:build-from-package: + desc: Builds the Docker image from the existing Python package in dist/ + deps: + - py:package + cmds: + - docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT={{.GIT_COMMIT}} --build-context packages=dist . + vars: + GIT_COMMIT: + sh: git rev-parse HEAD + docker:prod: desc: builds and runs the production docker image locally dir: docker diff --git a/docker/Dockerfile b/docker/Dockerfile index bdee7416e..92d9c48fe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,11 @@ -FROM node:16 as builder +############################################### +# Frontend Build +############################################### +FROM node:16 AS frontend-builder -WORKDIR /app +WORKDIR /frontend -COPY ./frontend . +COPY frontend . RUN yarn install \ --prefer-offline \ @@ -26,14 +29,10 @@ ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=100 \ - POETRY_HOME="/opt/poetry" \ - POETRY_VIRTUALENVS_IN_PROJECT=true \ - POETRY_NO_INTERACTION=1 \ - PYSETUP_PATH="/opt/pysetup" \ - VENV_PATH="/opt/pysetup/.venv" + VENV_PATH="/opt/mealie" -# prepend poetry and venv to path -ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" +# prepend venv to path +ENV PATH="$VENV_PATH/bin:$PATH" # create user account RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \ @@ -41,38 +40,81 @@ RUN useradd -u 911 -U -d $MEALIE_HOME -s /bin/bash abc \ && mkdir $MEALIE_HOME ############################################### -# Builder Image +# Backend Package Build ############################################### -FROM python-base as builder-base +FROM python-base AS backend-builder RUN apt-get update \ && apt-get install --no-install-recommends -y \ curl \ + && rm -rf /var/lib/apt/lists/* + +ENV POETRY_HOME="/opt/poetry" \ + POETRY_NO_INTERACTION=1 + +# prepend poetry to path +ENV PATH="$POETRY_HOME/bin:$PATH" + +# install poetry - respects $POETRY_VERSION & $POETRY_HOME +ENV POETRY_VERSION=2.0.1 +RUN curl -sSL https://install.python-poetry.org | python3 - + +# install poetry plugins needed to build the package +RUN poetry self add "poetry-plugin-export>=1.9" + +WORKDIR /mealie + +# copy project files here to ensure they will be cached. +COPY poetry.lock pyproject.toml ./ +COPY mealie ./mealie + +# Copy frontend to package it into the wheel +COPY --from=frontend-builder /frontend/dist ./mealie/frontend + +# Build the source and binary package +RUN poetry build --output=dist + +# Create the requirements file, which is used to install the built package and +# its pinned dependencies later. mealie is included to ensure the built one is +# what's installed. +RUN export MEALIE_VERSION=$(poetry version --short) \ + && poetry export --only=main --extras=pgsql --output=dist/requirements.txt \ + && echo "mealie[pgsql]==$MEALIE_VERSION \\" >> dist/requirements.txt \ + && poetry run pip hash dist/mealie-$MEALIE_VERSION-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt \ + && echo " \\" >> dist/requirements.txt \ + && poetry run pip hash dist/mealie-$MEALIE_VERSION.tar.gz | tail -n1 >> dist/requirements.txt + +############################################### +# Package Container +# Only role is to hold the packages, or be overriden by a --build-context flag. +############################################### +FROM scratch AS packages +COPY --from=backend-builder /mealie/dist / + +############################################### +# Python Virtual Environment Build +############################################### +# Install packages required to build the venv, in parallel to building the wheel +FROM python-base AS venv-builder-base +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ build-essential \ libpq-dev \ libwebp-dev \ # LDAP Dependencies libsasl2-dev libldap2-dev libssl-dev \ gnupg gnupg2 gnupg1 \ - && rm -rf /var/lib/apt/lists/* \ - && pip install -U --no-cache-dir pip + && rm -rf /var/lib/apt/lists/* +RUN python3 -m venv --upgrade-deps $VENV_PATH -# install poetry - respects $POETRY_VERSION & $POETRY_HOME -ENV POETRY_VERSION=1.3.1 -RUN curl -sSL https://install.python-poetry.org | python3 - +# Install the wheel and all dependencies into the venv +FROM venv-builder-base AS venv-builder -# copy project requirement files here to ensure they will be cached. -WORKDIR $PYSETUP_PATH -COPY ./poetry.lock ./pyproject.toml ./ +# Copy built package (wheel) and its dependency requirements +COPY --from=packages * /dist/ -# install runtime deps - uses $POETRY_VIRTUALENVS_IN_PROJECT internally -RUN poetry install -E pgsql --only main - -############################################### -# CRFPP Image -############################################### -FROM hkotel/crfpp as crfpp - -RUN echo "crfpp-container" +# Install the wheel with exact versions of dependencies into the venv +RUN . $VENV_PATH/bin/activate \ + && pip install --require-hashes -r /dist/requirements.txt --find-links /dist ############################################### # Production Image @@ -96,39 +138,15 @@ RUN apt-get update \ # create directory used for Docker Secrets RUN mkdir -p /run/secrets -# copying poetry and venv into image -COPY --from=builder-base $POETRY_HOME $POETRY_HOME -COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH - -ENV LD_LIBRARY_PATH=/usr/local/lib -COPY --from=crfpp /usr/local/lib/ /usr/local/lib -COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn -COPY --from=crfpp /usr/local/bin/crf_test /usr/local/bin/crf_test - -# copy backend -COPY ./mealie $MEALIE_HOME/mealie -COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/ - -# venv already has runtime deps installed we get a quicker install -WORKDIR $MEALIE_HOME -RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --only main -WORKDIR / - -# Grab CRF++ Model Release -RUN python $MEALIE_HOME/mealie/scripts/install_model.py +# Copy venv into image. It contains a fully-installed mealie backend and frontend. +COPY --from=venv-builder $VENV_PATH $VENV_PATH VOLUME [ "$MEALIE_HOME/data/" ] ENV APP_PORT=9000 EXPOSE ${APP_PORT} -HEALTHCHECK CMD python $MEALIE_HOME/mealie/scripts/healthcheck.py || exit 1 - -# ---------------------------------- -# Copy Frontend - -ENV STATIC_FILES=/spa/static -COPY --from=builder /app/dist ${STATIC_FILES} +HEALTHCHECK CMD python -m mealie.scripts.healthcheck || exit 1 ENV HOST 0.0.0.0 diff --git a/docker/entry.sh b/docker/entry.sh index 3acb00efc..cccc2ba9d 100644 --- a/docker/entry.sh +++ b/docker/entry.sh @@ -32,13 +32,51 @@ init() { cd /app # Activate our virtual environment here - . /opt/pysetup/.venv/bin/activate + . /opt/mealie/bin/activate +} + +load_secrets() { + # Each of these environment variables will support a `_FILE` suffix that allows + # for setting the environment variable through the Docker Compose secret + # pattern. + local -a secret_supported_vars=( + "POSTGRES_USER" + "POSTGRES_PASSWORD" + "POSTGRES_SERVER" + "POSTGRES_PORT" + "POSTGRES_DB" + "POSTGRES_URL_OVERRIDE" + + "SMTP_HOST" + "SMTP_PORT" + "SMTP_USER" + "SMTP_PASSWORD" + + "LDAP_SERVER_URL" + "LDAP_QUERY_PASSWORD" + + "OIDC_CONFIGURATION_URL" + "OIDC_CLIENT_ID" + "OIDC_CLIENT_SECRET" + + "OPENAI_BASE_URL" + "OPENAI_API_KEY" + ) + + # If any secrets are set, prefer them over base environment variables. + for var in "${secret_supported_vars[@]}"; do + file_var="${var}_FILE" + if [ -n "${!file_var}" ]; then + export "$var=$(<"${!file_var}")" + fi + done } change_user init +load_secrets # Start API HOST_IP=`/sbin/ip route|awk '/default/ { print $3 }'` -exec python /app/mealie/main.py +exec mealie diff --git a/docs/docs/contributors/developers-guide/building-packages.md b/docs/docs/contributors/developers-guide/building-packages.md new file mode 100644 index 000000000..5fe45e13c --- /dev/null +++ b/docs/docs/contributors/developers-guide/building-packages.md @@ -0,0 +1,40 @@ +# Building Packages + +Released packages are [built and published via GitHub actions](maintainers.md#drafting-releases). + +## Python packages + +To build Python packages locally for testing, use [`task`](starting-dev-server.md#without-dev-containers). After installing `task`, run `task py:package` to perform all the steps needed to build the package and a requirements file. To do it manually, run: +```sh +pushd frontend +yarnpkg install +yarnpkg generate +popd +rm -r mealie/frontend +cp -a frontend/dist mealie/frontend +poetry build +poetry export -n --only=main --extras=pgsql --output=dist/requirements.txt +MEALIE_VERSION=$(poetry version --short) +echo "mealie[pgsql]==${MEALIE_VERSION} \\" >> dist/requirements.txt +poetry run pip hash dist/mealie-${MEALIE_VERSION}-py3-none-any.whl | tail -n1 | tr -d '\n' >> dist/requirements.txt +echo " \\" >> dist/requirements.txt +poetry run pip hash dist/mealie-${MEALIE_VERSION}.tar.gz | tail -n1 >> dist/requirements.txt +``` + +The Python package can be installed with all of its dependencies pinned to the versions tested by the developers with: +```sh +pip3 install -r dist/requirements.txt --find-links dist +``` + +To install with the latest but still compatible dependency versions, instead run `pip3 install dist/mealie-$VERSION-py3-none-any.whl` (where `$VERSION` is the version of mealie to install). + +## Docker image +One way to build the Docker image is to run the following command in the project root directory: +```sh +docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) . +``` + +The Docker image can be built from the pre-built Python packages with the task command `task docker:build-from-package`. This is equivalent to: +```sh +docker build --tag mealie:dev --file docker/Dockerfile --build-arg COMMIT=$(git rev-parse HEAD) --build-context packages=dist . +``` diff --git a/docs/docs/documentation/community-guide/bring-api.md b/docs/docs/documentation/community-guide/bring-api.md new file mode 100644 index 000000000..41214bb0c --- /dev/null +++ b/docs/docs/documentation/community-guide/bring-api.md @@ -0,0 +1,8 @@ +!!! info +This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! + +Mealie supports adding the ingredients of a recipe to your [Bring](https://www.getbring.com/) shopping list, as you can +see [here](https://docs.mealie.io/documentation/getting-started/features/#recipe-actions). +However, for this to work, your Mealie instance needs to be exposed to the open Internet so that the Bring servers can access its information. If you don't want your server to be publicly accessible for security reasons, you can use the [Mealie-Bring-API](https://github.com/felixschndr/mealie-bring-api) written by a community member. This integration is entirely local and does not require any service to be exposed to the Internet. + +This is a small web server that runs locally next to your Mealie instance, and instead of Bring pulling the data from you, it pushes the data to Bring. [Check out the project](https://github.com/felixschndr/mealie-bring-api) for more information and installation instructions. diff --git a/docs/docs/documentation/getting-started/authentication/oidc-v2.md b/docs/docs/documentation/getting-started/authentication/oidc-v2.md index 35f67369e..ee8c3ba9b 100644 --- a/docs/docs/documentation/getting-started/authentication/oidc-v2.md +++ b/docs/docs/documentation/getting-started/authentication/oidc-v2.md @@ -10,7 +10,7 @@ Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many Identity Providers (IdP), including: - [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect) -- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/) +- [Authelia](https://www.authelia.com/integration/openid-connect/mealie/) - [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc) - [Okta](https://www.okta.com/openid-connect/) diff --git a/docs/docs/documentation/getting-started/faq.md b/docs/docs/documentation/getting-started/faq.md index 7035f3713..8cfc62848 100644 --- a/docs/docs/documentation/getting-started/faq.md +++ b/docs/docs/documentation/getting-started/faq.md @@ -65,6 +65,24 @@ 4. Click 'Update' +??? question "Why Link Ingredients to a Recipe Step?" + + ### Why Link Ingredients to a Recipe Step? + + Mealie allows you to link ingredients to specific steps in a recipe, ensuring you know exactly when to add each ingredient during the cooking process. + + **Link Ingredients to Steps in a Recipe** + + 1. Go to a recipe + 2. Click the Edit button/icon + 3. Scroll down to the step you want to link ingredients to + 4. Click the ellipsis button next to the step and click 'Link Ingredients' + 5. Check off the Ingredient(s) that you want to link to that step + 6. Optionally, click 'Next step' to continue linking remaining ingredients to steps, or click 'Save' to Finish + 7. Click 'Save' on the Recipe + + You can optionally link the same ingredient to multiple steps, which is useful for prepping an ingredient in one step and using it in another. + ??? question "What is fuzzy search and how do I use it?" ### What is fuzzy search and how do I use it? @@ -130,7 +148,7 @@ ```shell docker exec -it mealie bash - python /app/mealie/scripts/reset_locked_users.py + python /opt/mealie/lib/python3.12/site-packages/reset_locked_users.py ``` @@ -143,7 +161,7 @@ ```shell docker exec -it mealie bash - python /app/mealie/scripts/make_admin.py + python /opt/mealie/lib/python3.12/site-packages/make_admin.py ``` @@ -156,7 +174,7 @@ ```shell docker exec -it mealie bash - python /app/mealie/scripts/change_password.py + python /opt/mealie/lib/python3.12/site-packages/mealie/scripts/change_password.py ``` @@ -211,6 +229,15 @@ ## Security and Maintenance +??? question "How can I use Mealie externally?" + + ### How can I use Mealie externally + + Exposing Mealie or any service to the internet can pose significant security risks. Before proceeding, carefully evaluate the potential impacts on your system. Due to the unique nature of each network, we cannot provide specific steps for your setup. + + There is a community guide available for one way to potentially set this up, and you could reach out on Discord for further discussion on what may be best for your network. + + ??? question "Can I use fail2ban with Mealie?" ### Can I use fail2ban with Mealie? @@ -235,6 +262,19 @@ ## Technical Considerations + +??? question "Why setup Email?" + + ### Why setup Email? + + Mealie uses email to send account invites and password resets. If you don't use these features, you don't need to set up email. There are also other methods to perform these actions that do not require the setup of Email. + + Email settings can be adjusted via environment variables on the backend container: + + - [Backend Config](./installation/backend-config.md) + + Note that many email providers (e.g., Gmail, Outlook) are disabling SMTP Auth and requiring Modern Auth, which Mealie currently does not support. You may need to use an SMTP relay or third-party SMTP provider, such as SMTP2GO. + ??? question "Why an API?" ### Why an API? diff --git a/docs/docs/documentation/getting-started/features.md b/docs/docs/documentation/getting-started/features.md index 8e4aa6258..7962bb292 100644 --- a/docs/docs/documentation/getting-started/features.md +++ b/docs/docs/documentation/getting-started/features.md @@ -35,7 +35,7 @@ Mealie has a robust and flexible recipe organization system with a few different #### Categories -Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**. +Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, **Side**, or **Drinks**. [Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary } @@ -84,7 +84,30 @@ The meal planner has the concept of plan rules. These offer a flexible way to us The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week. +Managing shopping lists can be done from the Sidebar > Shopping Lists. +Here you will be able to: +- See items already on the Shopping List +- See linked recipes with ingredients + - Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it. +- Check off an item +- Add / Change / Remove / Sort Items via the grid icon + - Be sure if you are modifying an ingredient to click the 'Save' icon. +- Add / Change / Remove / Sort Labels + - 'No Label' will always be on the top, others can be Reordered via the 'Reorder Labels' button + +!!! tip + If you accidentally checked off an item, you can uncheck it by expanding 'items checked' and unchecking it. This will add it back to the Shopping List. + +!!! tip + You can use Labels to categorize your ingredients. You may want to Label by Food Type (Frozen, Fresh, etc), by Store, Tool, Recipe, or more. Play around with this to see what works best for you. + +!!! tip + You can toggle 'Food' on items so that if you add multiple of the same food / ingredient, Mealie will automatically combine them together. Do this by editing an item in the Shopping List and clicking the 'Apple' icon. If you then have recipes that contain "1 | cup | cheese" and "2 | cup | cheese" this would be combined to show "3 cups of cheese." + +[See FAQ for more information](../getting-started/faq.md) + + [Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary } ## Integrations @@ -94,9 +117,9 @@ Mealie is designed to integrate with many different external services. There are ### Notifiers Notifiers are event-driven notifications sent when specific actions are performed within Mealie. Some actions include: -- creating a recipe -- adding items to a shopping list -- creating a new mealplan +- Creating / Updating a recipe +- Adding items to a shopping list +- Creating a new mealplan Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), which integrates with a large number of notification services. In addition, certain custom notifiers send basic event data to the consumer (e.g. the `id` of the resource). These include: @@ -139,6 +162,9 @@ Below is a list of all valid merge fields: - ${id} - ${slug} - ${url} +- ${servings} +- ${yieldQuantity} +- ${yieldText} To add, modify, or delete Recipe Actions, visit the Data Management page (more on that below). diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md index 6884bd6f7..7cca09444 100644 --- a/docs/docs/documentation/getting-started/installation/backend-config.md +++ b/docs/docs/documentation/getting-started/installation/backend-config.md @@ -31,27 +31,27 @@ ### Database -| Variables | Default | Description | -| --------------------- | :------: | ----------------------------------------------------------------------- | -| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' | -| POSTGRES_USER | mealie | Postgres database user | -| POSTGRES_PASSWORD | mealie | Postgres database password | -| POSTGRES_SERVER | postgres | Postgres database server address | -| POSTGRES_PORT | 5432 | Postgres database port | -| POSTGRES_DB | mealie | Postgres database name | -| POSTGRES_URL_OVERRIDE | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables | + | Variables | Default | Description | + | ------------------------------------------------------- | :------: | ----------------------------------------------------------------------- | + | DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' | + | POSTGRES_USER[†][secrets] | mealie | Postgres database user | + | POSTGRES_PASSWORD[†][secrets] | mealie | Postgres database password | + | POSTGRES_SERVER[†][secrets] | postgres | Postgres database server address | + | POSTGRES_PORT[†][secrets] | 5432 | Postgres database port | + | POSTGRES_DB[†][secrets] | mealie | Postgres database name | + | POSTGRES_URL_OVERRIDE[†][secrets] | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables | ### Email -| Variables | Default | Description | -| ------------------ | :-----: | ------------------------------------------------- | -| SMTP_HOST | None | Required For email | -| SMTP_PORT | 587 | Required For email | -| SMTP_FROM_NAME | Mealie | Required For email | -| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' | -| SMTP_FROM_EMAIL | None | Required For email | -| SMTP_USER | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' | -| SMTP_PASSWORD | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' | +| Variables | Default | Description | +| ----------------------------------------------- | :-----: | ------------------------------------------------- | +| SMTP_HOST[†][secrets] | None | Required For email | +| SMTP_PORT[†][secrets] | 587 | Required For email | +| SMTP_FROM_NAME | Mealie | Required For email | +| SMTP_AUTH_STRATEGY | TLS | Required For email, Options: 'TLS', 'SSL', 'NONE' | +| SMTP_FROM_EMAIL | None | Required For email | +| SMTP_USER[†][secrets] | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' | +| SMTP_PASSWORD[†][secrets] | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' | ### Webworker @@ -72,21 +72,21 @@ Use this only when mealie is run without a webserver or reverse proxy. ### LDAP -| Variables | Default | Description | -| -------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- | -| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth | -| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) | -| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP | -| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) | -| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server | -| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) | -| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used | -| LDAP_QUERY_PASSWORD | None | Optional password for the bind user used in LDAP_QUERY_BIND | -| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) | -| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) | -| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id | -| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name | -| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email | +| Variables | Default | Description | +| ----------------------------------------------------- | :-----: | ----------------------------------------------------------------------------------------------------------------------------------- | +| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth | +| LDAP_SERVER_URL[†][secrets] | None | LDAP server URL (e.g. ldap://ldap.example.com) | +| LDAP_TLS_INSECURE | False | Do not verify server certificate when using secure LDAP | +| LDAP_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) | +| LDAP_ENABLE_STARTTLS | False | Optional. Use STARTTLS to connect to the server | +| LDAP_BASE_DN | None | Starting point when searching for users authentication (e.g. `CN=Users,DC=xx,DC=yy,DC=de`) | +| LDAP_QUERY_BIND | None | Optional bind user for LDAP search queries (e.g. `cn=admin,cn=users,dc=example,dc=com`). If `None` then anonymous bind will be used | +| LDAP_QUERY_PASSWORD[†][secrets] | None | Optional password for the bind user used in LDAP_QUERY_BIND | +| LDAP_USER_FILTER | None | Optional LDAP filter to narrow down eligible users (e.g. `(memberOf=cn=mealie_user,dc=example,dc=com)`) | +| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) | +| LDAP_ID_ATTRIBUTE | uid | The LDAP attribute that maps to the user's id | +| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name | +| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email | ### OpenID Connect (OIDC) @@ -94,23 +94,24 @@ Use this only when mealie is run without a webserver or reverse proxy. For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md) -| Variables | Default | Description | -|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect | -| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC | -| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration | -| OIDC_CLIENT_ID | None | The client id of your configured client in your provider | -| OIDC_CLIENT_SECRET
:octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider | -| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) | -| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) | -| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL | -| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with " | -| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked | -| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") | -| OIDC_NAME_CLAIM | name | This is the claim which Mealie will use for the users Full Name | -| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** | -| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. | -| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) | +| Variables | Default | Description | +| ----------------------------------------------------------------------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect | +| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC | +| OIDC_CONFIGURATION_URL[†][secrets] | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration | +| OIDC_CLIENT_ID[†][secrets] | None | The client id of your configured client in your provider | +| OIDC_CLIENT_SECRET[†][secrets]
:octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider | +| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate, regardless of the `OIDC_ADMIN_GROUP`. For more information see [this page](../authentication/oidc.md#groups) | +| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be made an admin. For more information see [this page](../authentication/oidc.md#groups) | +| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed an you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL | +| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with " | +| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked | +| OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | +| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") | +| OIDC_NAME_CLAIM | name | This is the claim which Mealie will use for the users Full Name | +| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** | +| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. | +| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) | ### OpenAI @@ -119,17 +120,17 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md) Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md). For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`) -| Variables | Default | Description | -| ---------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform | -| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features | -| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty | -| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them | -| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them | -| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs | -| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs | -| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs | -| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware | +| Variables | Default | Description | +| ------------------------------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OPENAI_BASE_URL[†][secrets] | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform | +| OPENAI_API_KEY[†][secrets] | None | Your OpenAI API Key. Enables OpenAI-related features | +| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty | +| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them | +| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them | +| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs | +| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs | +| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs | +| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware | ### Theming @@ -154,24 +155,80 @@ Setting the following environmental variables will change the theme of the front ### Docker Secrets -Setting a credential can be done using secrets when running in a Docker container. -This can be used to avoid leaking passwords through compose files, environment variables, or command-line history. -For example, to configure the Postgres database password in Docker compose, create a file on the host that contains only the password, and expose that file to the Mealie service as a secret with the correct name. -Note that environment variables take priority over secrets, so any previously defined environment variables should be removed when migrating to secrets. +### Docker Secrets + +> Starting in version `2.4.2`, any environment variable in the preceding lists with a dagger +> symbol next to them support the Docker Compose secrets pattern, below. +[Docker Compose secrets][docker-secrets] can be used to secure sensitive information regarding the Mealie implementation +by managing control of each secret independently from the single `.env` file. This is helpful for users that may need +different levels of access for various, sensitive environment variables, such as differentiating between hardening +operations (e.g., server endpoints and ports) and user access control (e.g., usernames, passwords, and API keys). + +To convert any of these environment variables to a Docker Compose secret, append `_FILE` to the environment variable and +connect it with a Docker Compose secret, per the [Docker documentation][docker-secrets]. + +If both the base environment variable and the secret pattern of the environment variable are set, the secret will always +take precedence. + +For example, a user that wishes to harden their operations by only giving some access to their database URL, but who +wish to place additional security around their user access control, may have a Docker Compose configuration similar to: + ```yaml services: mealie: - ... - environment: - ... - POSTGRES_USER: postgres secrets: - - POSTGRES_PASSWORD + # These secrets will be loaded by Docker into the `/run/secrets` folder within the container. + - postgres-host + - postgres-port + - postgres-db-name + - postgres-user + - postgres-password + environment: + DB_ENGINE: postgres + POSTGRES_SERVER: duplicate.entry.tld # This will be ignored, due to the secret defined, below. + POSTGRES_SERVER_FILE: /run/secrets/postgres-host + POSTGRES_PORT_FILE: /run/secrets/postgres-port + POSTGRES_DB_FILE: /run/secrets/postgres-db-name + POSTGRES_USER_FILE: /run/secrets/postgres-user + POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password + +# Each of these secrets are loaded via these local files. Different patterns are available. See the Docker Compose +# documentation for more information. secrets: - POSTGRES_PASSWORD: - file: postgrespassword.txt + postgres-host: + file: ./secrets/postgres-host.txt + postgres-port: + file: ./secrets/postgres-port.txt + postgres-db-name: + file: ./secrets/sensitive/postgres-db-name.txt + postgres-user: + file: ./secrets/sensitive/postgres-user.txt + postgres-password: + file: ./secrets/sensitive/postgres-password.txt +``` +In the example above, a directory organization and access pattern may look like the following: +```text +. +โ”œโ”€โ”€ docker-compose.yml +โ””โ”€โ”€ secrets # Access restricted to anyone that can manage secrets + โ”œโ”€โ”€ postgres-host.txt + โ”œโ”€โ”€ postgres-port.txt + โ””โ”€โ”€ sensitive # Access further-restricted to anyone managing service accounts + โ”œโ”€โ”€ postgres-db-name.txt + โ”œโ”€โ”€ postgres-password.txt + โ””โ”€โ”€ postgres-user.txt ``` +How you organize your secrets is ultimately up to you. At minimum, it's highly recommended to use secret patterns for +at least these sensitive environment variables when working within shared environments: + +- `POSTGRES_PASSWORD` +- `SMTP_PASSWORD` +- `LDAP_QUERY_PASSWORD` +- `OPENAI_API_KEY` + +[docker-secrets]: https://docs.docker.com/compose/use-secrets/ +[secrets]: #docker-secrets [unicorn_workers]: https://www.uvicorn.org/deployment/#built-in diff --git a/docs/docs/documentation/getting-started/installation/installation-checklist.md b/docs/docs/documentation/getting-started/installation/installation-checklist.md index 1cf2500f7..bf5ef93de 100644 --- a/docs/docs/documentation/getting-started/installation/installation-checklist.md +++ b/docs/docs/documentation/getting-started/installation/installation-checklist.md @@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do: 1. Take a backup just in case! -2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.4.2` +2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.8.0` 3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access. 4. Restart the container diff --git a/docs/docs/documentation/getting-started/installation/postgres.md b/docs/docs/documentation/getting-started/installation/postgres.md index dac2231c4..4e08fa192 100644 --- a/docs/docs/documentation/getting-started/installation/postgres.md +++ b/docs/docs/documentation/getting-started/installation/postgres.md @@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In ```yaml services: mealie: - image: ghcr.io/mealie-recipes/mealie:v2.4.2 # (3) + image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3) container_name: mealie restart: always ports: diff --git a/docs/docs/documentation/getting-started/installation/sqlite.md b/docs/docs/documentation/getting-started/installation/sqlite.md index 49d2dd6f9..f33080e0e 100644 --- a/docs/docs/documentation/getting-started/installation/sqlite.md +++ b/docs/docs/documentation/getting-started/installation/sqlite.md @@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th ```yaml services: mealie: - image: ghcr.io/mealie-recipes/mealie:v2.4.2 # (3) + image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3) container_name: mealie restart: always ports: diff --git a/docs/docs/documentation/getting-started/usage/backups-and-restoring.md b/docs/docs/documentation/getting-started/usage/backups-and-restoring.md index 84b9317b2..7f10ea786 100644 --- a/docs/docs/documentation/getting-started/usage/backups-and-restoring.md +++ b/docs/docs/documentation/getting-started/usage/backups-and-restoring.md @@ -1,17 +1,24 @@ -# Backups and Restoring +# Backups and Restores -Mealie provides an integrated mechanics for doing full installation backups of the database. Navigate to `/admin/backups` to +Mealie provides an integrated mechanic for doing full installation backups of the database. + +Navigate to Settings > Backups or manually by adding `/admin/backups` to your instance URL. + +From this page, you will be able to: - See a list of available backups -- Perform a backups -- Restore a backup +- Create a backup +- Upload a backup +- Delete a backup (Confirmation Required) +- Download a backup +- Perform a restore !!! tip If you're using Mealie with SQLite all your data is stored in the /app/data/ folder in the container. You can easily perform entire site backups by stopping the container, and backing up this folder with your chosen tool. This is the **best** way to backup your data. ## Restoring from a Backup -To restore from a backup it needs to be uploaded to your instance, this can be done through the web portal. On the lower left hand corner of the backups data table you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly. +To restore from a backup it needs to be uploaded to your instance which can be done through the web portal. On the top left of the page you'll see an upload button. Click this button and select the backup file you want to upload and it will be available to import shortly. You can alternatively use one of the backups you see on the screen, if one exists. Before importing it's critical that you understand the following: @@ -19,6 +26,9 @@ Before importing it's critical that you understand the following: - This action cannot be undone - If this action is successful you will be logged out and you will need to log back in to complete the restore +!!! tip + If for some reason the restore does not succeed, you can review the logs of what the issue may be, download the backup .ZIP and edit the contents of database.json to potentially resolve the issue. For example, if you receive an error restoring 'shopping-list' you can edit out the contents of that list while allowing other sections to restore. If you would like any assistance on this, reach out over Discord. + !!! warning Prior to beta-v5 using a mis-matched version of the database backup will result in an error that will prevent you from using the instance of Mealie requiring you to remove all data and reinstall. Post beta-v5 performing a mismatched restore will throw an error and alert the user of the issue. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 0351da02a..64a86f4da 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -85,12 +85,13 @@ nav: - OpenID Connect: "documentation/getting-started/authentication/oidc-v2.md" - Community Guides: + - Bring API without internet exposure: "documentation/community-guide/bring-api.md" + - Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md" + - Bulk Url Import: "documentation/community-guide/bulk-url-import.md" + - Home Assistant: "documentation/community-guide/home-assistant.md" + - Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md" - iOS Shortcuts: "documentation/community-guide/ios.md" - Reverse Proxy (SWAG): "documentation/community-guide/swag.md" - - Home Assistant: "documentation/community-guide/home-assistant.md" - - Bulk Url Import: "documentation/community-guide/bulk-url-import.md" - - Import Bookmarklet: "documentation/community-guide/import-recipe-bookmarklet.md" - - Automate Backups with n8n: "documentation/community-guide/n8n-backup-automation.md" - API Reference: "api/redoc.md" @@ -98,6 +99,7 @@ nav: - Non-Code: "contributors/non-coders.md" - Translating: "contributors/translating.md" - Developers Guide: + - Building Packages: "contributors/developers-guide/building-packages.md" - Code Contributions: "contributors/developers-guide/code-contributions.md" - Dev Getting Started: "contributors/developers-guide/starting-dev-server.md" - Database Changes: "contributors/developers-guide/database-changes.md" diff --git a/frontend/components/Domain/Cookbook/CookbookPage.vue b/frontend/components/Domain/Cookbook/CookbookPage.vue index 5d771ad52..97964db3c 100644 --- a/frontend/components/Domain/Cookbook/CookbookPage.vue +++ b/frontend/components/Domain/Cookbook/CookbookPage.vue @@ -104,9 +104,12 @@ } const response = await actions.updateOne(editTarget.value); - // if name changed, redirect to new slug if (response?.slug && book.value?.slug !== response?.slug) { + // if name changed, redirect to new slug router.push(`/g/${route.value.params.groupSlug}/cookbooks/${response?.slug}`); + } else { + // otherwise reload the page, since the recipe criteria changed + router.go(0); } dialogStates.edit = false; editTarget.value = null; diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue index 98611b02c..64fb9c0b3 100644 --- a/frontend/components/Domain/Recipe/RecipeCardSection.vue +++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue @@ -219,27 +219,34 @@ export default defineComponent({ const router = useRouter(); const queryFilter = computed(() => { - const orderBy = props.query?.orderBy || preferences.value.orderBy; - const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null; + return props.query.queryFilter || null; - if (props.query.queryFilter && orderByFilter) { - return `(${props.query.queryFilter}) AND ${orderByFilter}`; - } else if (props.query.queryFilter) { - return props.query.queryFilter; - } else { - return orderByFilter; - } + // TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade) + + // const orderBy = props.query?.orderBy || preferences.value.orderBy; + // const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null; + + // if (props.query.queryFilter && orderByFilter) { + // return `(${props.query.queryFilter}) AND ${orderByFilter}`; + // } else if (props.query.queryFilter) { + // return props.query.queryFilter; + // } else { + // return orderByFilter; + // } }); async function fetchRecipes(pageCount = 1) { + const orderDir = props.query?.orderDirection || preferences.value.orderDirection; + const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last"; return await fetchMore( page.value, perPage * pageCount, props.query?.orderBy || preferences.value.orderBy, - props.query?.orderDirection || preferences.value.orderDirection, + orderDir, + orderByNullPosition, props.query, // we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by - queryFilter.value + queryFilter.value, ); } diff --git a/frontend/components/Domain/Recipe/RecipeContextMenu.vue b/frontend/components/Domain/Recipe/RecipeContextMenu.vue index a0a47832c..b6165feff 100644 --- a/frontend/components/Domain/Recipe/RecipeContextMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeContextMenu.vue @@ -371,7 +371,7 @@ export default defineComponent({ const groupRecipeActionsStore = useGroupRecipeActions(); async function executeRecipeAction(action: GroupRecipeActionOut) { - const response = await groupRecipeActionsStore.execute(action, props.recipe); + const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale); if (action.actionType === "post") { if (!response?.error) { diff --git a/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue b/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue index 6f233b84f..0aae51731 100644 --- a/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue +++ b/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue @@ -138,8 +138,8 @@ import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"; import { useUserApi } from "~/composables/api"; import { alert } from "~/composables/use-toast"; import { useShoppingListPreferences } from "~/composables/use-users/preferences"; -import { ShoppingListSummary } from "~/lib/api/types/household"; -import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe"; +import { RecipeIngredient, ShoppingListAddRecipeParamsBulk, ShoppingListSummary } from "~/lib/api/types/household"; +import { Recipe } from "~/lib/api/types/recipe"; export interface RecipeWithScale extends Recipe { scale: number; @@ -342,12 +342,12 @@ export default defineComponent({ } async function addRecipesToList() { - const promises: Promise[] = []; - recipeIngredientSections.value.forEach((section) => { - if (!selectedShoppingList.value) { - return; - } + if (!selectedShoppingList.value) { + return; + } + const recipeData: ShoppingListAddRecipeParamsBulk[] = []; + recipeIngredientSections.value.forEach((section) => { const ingredients: RecipeIngredient[] = []; section.ingredientSections.forEach((ingSection) => { ingSection.ingredients.forEach((ing) => { @@ -361,24 +361,18 @@ export default defineComponent({ return; } - promises.push(api.shopping.lists.addRecipe( - selectedShoppingList.value.id, - section.recipeId, - section.recipeScale, - ingredients, - )); + recipeData.push( + { + recipeId: section.recipeId, + recipeIncrementQuantity: section.recipeScale, + recipeIngredients: ingredients, + } + ); }); - let success = true; - const results = await Promise.allSettled(promises); - results.forEach((result) => { - if (result.status === "rejected") { - success = false; - } - }) - - success ? alert.success(i18n.tc("recipe.successfully-added-to-list")) - : alert.error(i18n.tc("failed-to-add-recipes-to-list")) + const { error } = await api.shopping.lists.addRecipes(selectedShoppingList.value.id, recipeData); + error ? alert.error(i18n.tc("recipe.failed-to-add-recipes-to-list")) + : alert.success(i18n.tc("recipe.successfully-added-to-list")); state.shoppingListDialog = false; state.shoppingListIngredientDialog = false; diff --git a/frontend/components/Domain/Recipe/RecipeLastMade.vue b/frontend/components/Domain/Recipe/RecipeLastMade.vue index 178db28bd..585075d46 100644 --- a/frontend/components/Domain/Recipe/RecipeLastMade.vue +++ b/frontend/components/Domain/Recipe/RecipeLastMade.vue @@ -86,29 +86,27 @@
-
- - - {{ $globals.icons.calendar }} - -
- {{ $t('recipe.last-made-date', { date: lastMade ? new Date(lastMade).toLocaleDateString($i18n.locale) : $t("general.never") } ) }} -
-
- -
-
-
-
- - - {{ $t('recipe.made-this') }} - +
+ + + + {{ $tc("recipe.made-this") }} + +
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue index d8ce45752..5e11391cd 100644 --- a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue @@ -14,15 +14,16 @@ - - -
- + + +
+ @@ -31,18 +32,18 @@
-
+
diff --git a/frontend/components/Domain/Recipe/RecipePrintView.vue b/frontend/components/Domain/Recipe/RecipePrintView.vue index 01e5b45dd..e1d792cb5 100644 --- a/frontend/components/Domain/Recipe/RecipePrintView.vue +++ b/frontend/components/Domain/Recipe/RecipePrintView.vue @@ -30,12 +30,17 @@
- + + + + diff --git a/frontend/components/Domain/Recipe/RecipeTimeCard.vue b/frontend/components/Domain/Recipe/RecipeTimeCard.vue index 9758c4319..4aa555ce5 100644 --- a/frontend/components/Domain/Recipe/RecipeTimeCard.vue +++ b/frontend/components/Domain/Recipe/RecipeTimeCard.vue @@ -1,41 +1,37 @@ -