mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 22:43:34 -07:00
Merge branch 'mealie-next' into rearrange-parse-button
This commit is contained in:
commit
47493ecf4a
47 changed files with 1352 additions and 692 deletions
|
@ -6,7 +6,7 @@
|
|||
.idea
|
||||
.vscode
|
||||
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
@ -25,9 +25,11 @@ venv
|
|||
|
||||
*/node_modules
|
||||
*/dist
|
||||
/dist/
|
||||
*/data/db
|
||||
*/mealie/test
|
||||
*/mealie/.temp
|
||||
/mealie/frontend/
|
||||
|
||||
model.crfmodel
|
||||
|
||||
|
|
14
.github/workflows/e2e.yml
vendored
14
.github/workflows/e2e.yml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/partial-backend.yml
vendored
2
.github/workflows/partial-backend.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Backend Test/Lint
|
||||
name: Backend Lint and Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
|
19
.github/workflows/partial-builder.yml
vendored
19
.github/workflows/partial-builder.yml
vendored
|
@ -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: |
|
||||
|
|
36
.github/workflows/partial-frontend.yml
vendored
36
.github/workflows/partial-frontend.yml
vendored
|
@ -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"
|
||||
|
|
102
.github/workflows/partial-package.yml
vendored
Normal file
102
.github/workflows/partial-package.yml
vendored
Normal file
|
@ -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
|
2
.github/workflows/pull-requests.yml
vendored
2
.github/workflows/pull-requests.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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:
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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.
|
||||
|
|
|
@ -12,7 +12,7 @@ repos:
|
|||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.4
|
||||
rev: v0.9.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -60,5 +60,9 @@
|
|||
},
|
||||
"[vue]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
}
|
||||
|
|
86
Taskfile.yml
86
Taskfile.yml
|
@ -41,14 +41,25 @@ 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
|
||||
sources:
|
||||
- poetry.lock
|
||||
- pyproject.toml
|
||||
- .pre-commit-config.yaml
|
||||
|
||||
setup:model:
|
||||
desc: setup nlp model
|
||||
|
@ -131,6 +142,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 +228,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 +260,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
|
||||
|
|
|
@ -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,31 +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
|
||||
# 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
|
||||
|
||||
###############################################
|
||||
# CRFPP Image
|
||||
|
@ -96,39 +145,25 @@ 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
|
||||
|
||||
# copy CRF++ and add it to the library 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/
|
||||
# Copy venv into image. It contains a fully-installed mealie backend and frontend.
|
||||
COPY --from=venv-builder $VENV_PATH $VENV_PATH
|
||||
|
||||
# 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
|
||||
RUN python -m mealie.scripts.install_model
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
40
docs/docs/contributors/developers-guide/building-packages.md
Normal file
40
docs/docs/contributors/developers-guide/building-packages.md
Normal file
|
@ -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 .
|
||||
```
|
|
@ -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<super>[†][secrets]</super> | mealie | Postgres database user |
|
||||
| POSTGRES_PASSWORD<super>[†][secrets]</super> | mealie | Postgres database password |
|
||||
| POSTGRES_SERVER<super>[†][secrets]</super> | postgres | Postgres database server address |
|
||||
| POSTGRES_PORT<super>[†][secrets]</super> | 5432 | Postgres database port |
|
||||
| POSTGRES_DB<super>[†][secrets]</super> | mealie | Postgres database name |
|
||||
| POSTGRES_URL_OVERRIDE<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | None | Required For email |
|
||||
| SMTP_PORT<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | None | Required if SMTP_AUTH_STRATEGY is 'TLS' or 'SSL' |
|
||||
| SMTP_PASSWORD<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | 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,22 @@ 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 <br/> :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_PROVIDER_NAME\>" |
|
||||
| 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<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | None | The client id of your configured client in your provider |
|
||||
| OIDC_CLIENT_SECRET<super>[†][secrets]</super> <br/> :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_PROVIDER_NAME\>" |
|
||||
| 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_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_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
|
||||
### OpenAI
|
||||
|
||||
|
@ -119,17 +118,13 @@ 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<super>[†][secrets]</super> | 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<super>[†][secrets]</super> | 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_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 |
|
||||
|
||||
### Theming
|
||||
|
||||
|
@ -154,24 +149,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
|
||||
|
||||
> <super>†</super> 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
|
||||
|
|
|
@ -99,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"
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
:label="$t('shopping-list.note')"
|
||||
rows="1"
|
||||
auto-grow
|
||||
autofocus
|
||||
@keypress="handleNoteKeyPress"
|
||||
></v-textarea>
|
||||
</div>
|
||||
|
@ -80,37 +81,37 @@
|
|||
<v-spacer />
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
...(allowDelete ? [{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
}] : []),
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
event: 'cancel',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.foods,
|
||||
text: $t('shopping-list.toggle-food'),
|
||||
event: 'toggle-foods',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@save="$emit('save')"
|
||||
@cancel="$emit('cancel')"
|
||||
@delete="$emit('delete')"
|
||||
@toggle-foods="listItem.isFood = !listItem.isFood"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card-actions class="ma-0 pt-0 pb-1 justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.close,
|
||||
text: $t('general.cancel'),
|
||||
event: 'cancel',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.foods,
|
||||
text: $t('shopping-list.toggle-food'),
|
||||
event: 'toggle-foods',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
text: $t('general.save'),
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@save="$emit('save')"
|
||||
@cancel="$emit('cancel')"
|
||||
@delete="$emit('delete')"
|
||||
@toggle-foods="listItem.isFood = !listItem.isFood"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -139,6 +140,11 @@ export default defineComponent({
|
|||
type: Array as () => IngredientFood[],
|
||||
required: true,
|
||||
},
|
||||
allowDelete: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const foodStore = useFoodStore();
|
||||
|
|
|
@ -550,7 +550,7 @@
|
|||
"yields-amount-with-text": "Yields {amount} {text}",
|
||||
"yield-text": "Yield Text",
|
||||
"quantity": "Quantity",
|
||||
"choose-unit": "Choose Unit",
|
||||
"choose-unit": "اختر الوحدة",
|
||||
"press-enter-to-create": "Press Enter to Create",
|
||||
"choose-food": "Choose Food",
|
||||
"notes": "Notes",
|
||||
|
|
|
@ -182,7 +182,7 @@
|
|||
"date": "Data",
|
||||
"id": "Id",
|
||||
"owner": "Dono",
|
||||
"change-owner": "Change Owner",
|
||||
"change-owner": "Mudar Proprietario",
|
||||
"date-added": "Engadida o",
|
||||
"none": "Nada",
|
||||
"run": "Executar",
|
||||
|
@ -214,10 +214,10 @@
|
|||
"confirm-delete-generic-items": "Estás seguro de que queres eliminar os seguintes elementos?",
|
||||
"organizers": "Organizadores",
|
||||
"caution": "Coidado",
|
||||
"show-advanced": "Show Advanced",
|
||||
"add-field": "Add Field",
|
||||
"show-advanced": "Mostrar Avanzadas",
|
||||
"add-field": "Adicionar Campo",
|
||||
"date-created": "Date Created",
|
||||
"date-updated": "Date Updated"
|
||||
"date-updated": "Data de Atualización"
|
||||
},
|
||||
"group": {
|
||||
"are-you-sure-you-want-to-delete-the-group": "Estás seguro de que queres eliminar <b>{groupName}<b/>?",
|
||||
|
@ -295,11 +295,11 @@
|
|||
"private-household-description": "Setting your household to private will disable all public view options. This overrides any individual public view settings",
|
||||
"lock-recipe-edits-from-other-households": "Lock recipe edits from other households",
|
||||
"lock-recipe-edits-from-other-households-description": "When enabled only users in your household can edit recipes created by your household",
|
||||
"household-recipe-preferences": "Household Recipe Preferences",
|
||||
"household-recipe-preferences": "Preferencias de receitas da casa",
|
||||
"default-recipe-preferences-description": "These are the default settings when a new recipe is created in your household. These can be changed for individual recipes in the recipe settings menu.",
|
||||
"allow-users-outside-of-your-household-to-see-your-recipes": "Allow users outside of your household to see your recipes",
|
||||
"allow-users-outside-of-your-household-to-see-your-recipes-description": "When enabled you can use a public share link to share specific recipes without authorizing the user. When disabled, you can only share recipes with users who are in your household or with a pre-generated private link",
|
||||
"household-preferences": "Household Preferences"
|
||||
"household-preferences": "Preferencias da Casa"
|
||||
},
|
||||
"meal-plan": {
|
||||
"create-a-new-meal-plan": "Crea un Novo Menú",
|
||||
|
@ -322,9 +322,9 @@
|
|||
"mealplan-update-failed": "Produciuse un erro na actualización do menú",
|
||||
"mealplan-updated": "Menú Actualizado",
|
||||
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
|
||||
"any-category": "Any Category",
|
||||
"any-tag": "Any Tag",
|
||||
"any-household": "Any Household",
|
||||
"any-category": "Calquer Categoria",
|
||||
"any-tag": "Calquer Etiqueta",
|
||||
"any-household": "Calquer Casa",
|
||||
"no-meal-plan-defined-yet": "Aínda non se definiu ningún menú",
|
||||
"no-meal-planned-for-today": "Non hai ningunha comida prevista para hoxe",
|
||||
"numberOfDays-hint": "Número de días ao cargar a páxina",
|
||||
|
@ -395,7 +395,7 @@
|
|||
},
|
||||
"tandoor": {
|
||||
"description-long": "Mealie can import recipes from Tandoor. Export your data in the \"Default\" format, then upload the .zip below.",
|
||||
"title": "Tandoor Recipes"
|
||||
"title": "Receitas do Tandoor"
|
||||
},
|
||||
"recipe-data-migrations": "Recipe Data Migrations",
|
||||
"recipe-data-migrations-explanation": "Recipes can be migrated from another supported application to Mealie. This is a great way to get started with Mealie.",
|
||||
|
@ -404,8 +404,8 @@
|
|||
"tag-all-recipes": "Tag all recipes with {tag-name} tag",
|
||||
"nextcloud-text": "Nextcloud recipes can be imported from a zip file that contains the data stored in Nextcloud. See the example folder structure below to ensure your recipes are able to be imported.",
|
||||
"chowdown-text": "Mealie admite de forma nativa o formato do repositorio de chowdown. Descarga o repositorio de códigos como ficheiro .zip e cárgao a continuación.",
|
||||
"recipe-1": "Recipe 1",
|
||||
"recipe-2": "Recipe 2",
|
||||
"recipe-1": "Receita 1",
|
||||
"recipe-2": "Receita 2",
|
||||
"paprika-text": "Mealie can import recipes from the Paprika application. Export your recipes from paprika, rename the export extension to .zip and upload it below.",
|
||||
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
|
||||
"plantoeat": {
|
||||
|
@ -424,15 +424,15 @@
|
|||
"new-recipe": {
|
||||
"bulk-add": "Bulk Add",
|
||||
"error-details": "Only websites containing ld+json or microdata can be imported by Mealie. Most major recipe websites support this data structure. If your site cannot be imported but there is json data in the log, please submit a github issue with the URL and data.",
|
||||
"error-title": "Looks Like We Couldn't Find Anything",
|
||||
"from-url": "Import a Recipe",
|
||||
"github-issues": "GitHub Issues",
|
||||
"error-title": "Parece que non conseguimos encontrar nada",
|
||||
"from-url": "Importar unha Receita",
|
||||
"github-issues": "Problemas no GitHub",
|
||||
"google-ld-json-info": "Google ld+json Info",
|
||||
"must-be-a-valid-url": "Must be a Valid URL",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
|
||||
"recipe-markup-specification": "Recipe Markup Specification",
|
||||
"recipe-url": "Recipe URL",
|
||||
"recipe-html-or-json": "Recipe HTML or JSON",
|
||||
"must-be-a-valid-url": "Precisa ser un URL válido",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Pegue os datos da sua receita. Cada liña será tratada como un item nunha lista",
|
||||
"recipe-markup-specification": "Especificación Markup da Receita",
|
||||
"recipe-url": "URL da Receita",
|
||||
"recipe-html-or-json": "Receita en HTML ou JSON",
|
||||
"upload-a-recipe": "Upload a Recipe",
|
||||
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
|
||||
"url-form-hint": "Copy and paste a link from your favorite recipe website",
|
||||
|
@ -440,13 +440,13 @@
|
|||
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
|
||||
"trim-prefix-description": "Trim first character from each line",
|
||||
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns",
|
||||
"import-by-url": "Import a recipe by URL",
|
||||
"import-by-url": "Importar unha receita por URL",
|
||||
"create-manually": "Create a recipe manually",
|
||||
"make-recipe-image": "Make this the recipe image"
|
||||
},
|
||||
"page": {
|
||||
"404-page-not-found": "404 Page not found",
|
||||
"all-recipes": "All Recipes",
|
||||
"404-page-not-found": "404 Páxina non encontrada",
|
||||
"all-recipes": "Todas as receitas",
|
||||
"new-page-created": "New page created",
|
||||
"page": "Páxina",
|
||||
"page-creation-failed": "Produciuse un erro ao creala páxina",
|
||||
|
@ -467,7 +467,7 @@
|
|||
"calories-suffix": "calorías",
|
||||
"carbohydrate-content": "Carbohidratos",
|
||||
"categories": "Categorías",
|
||||
"cholesterol-content": "Cholesterol",
|
||||
"cholesterol-content": "Colesterol",
|
||||
"comment-action": "Comentar",
|
||||
"comment": "Comentario",
|
||||
"comments": "Comentarios",
|
||||
|
@ -535,7 +535,7 @@
|
|||
"add-recipe-to-mealplan": "Add Recipe to Mealplan",
|
||||
"entry-type": "Entry Type",
|
||||
"date-format-hint": "Formato MM/DD/YYYY",
|
||||
"date-format-hint-yyyy-mm-dd": "YYYY-MM-DD format",
|
||||
"date-format-hint-yyyy-mm-dd": "Formato AAAA-MM-DD",
|
||||
"add-to-list": "Add to List",
|
||||
"add-to-plan": "Add to Plan",
|
||||
"add-to-timeline": "Add to Timeline",
|
||||
|
@ -553,7 +553,7 @@
|
|||
"choose-unit": "Choose Unit",
|
||||
"press-enter-to-create": "Press Enter to Create",
|
||||
"choose-food": "Choose Food",
|
||||
"notes": "Notes",
|
||||
"notes": "Notas",
|
||||
"toggle-section": "Toggle Section",
|
||||
"see-original-text": "See Original Text",
|
||||
"original-text-with-value": "Original Text: {originalText}",
|
||||
|
@ -1194,8 +1194,8 @@
|
|||
"demo_password": "Password: {password}"
|
||||
},
|
||||
"ocr-editor": {
|
||||
"ocr-editor": "Ocr editor",
|
||||
"toolbar": "Toolbar",
|
||||
"ocr-editor": "Editor OCR",
|
||||
"toolbar": "Barra de ferramentas",
|
||||
"selection-mode": "Selection mode",
|
||||
"pan-and-zoom-picture": "Pan and zoom picture",
|
||||
"split-text": "Split text",
|
||||
|
@ -1203,7 +1203,7 @@
|
|||
"split-by-block": "Split by text block",
|
||||
"flatten": "Flatten regardless of original formating",
|
||||
"help": {
|
||||
"help": "Help",
|
||||
"help": "Axuda",
|
||||
"mouse-modes": "Mouse modes",
|
||||
"selection-mode": "Selection Mode (default)",
|
||||
"selection-mode-desc": "The selection mode is the main mode that can be used to enter data:",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"category-filter": "Kategorifilter",
|
||||
"category-update-failed": "Kategori gick inte att uppdatera",
|
||||
"category-updated": "Kategori uppdaterad",
|
||||
"uncategorized-count": "Ingen Kategori {count}",
|
||||
"uncategorized-count": "Utan kategori {count}",
|
||||
"create-a-category": "Skapa kategori",
|
||||
"category-name": "Kategorinamn",
|
||||
"category": "Kategori"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"api-docs": "Документація API",
|
||||
"api-port": "Порт API",
|
||||
"application-mode": "Режим додатку",
|
||||
"database-type": "Тип бази данних",
|
||||
"database-type": "Тип бази даних",
|
||||
"database-url": "URL-адреса бази даних",
|
||||
"default-group": "Групи за замовчуванням",
|
||||
"default-household": "Сімʼя за замовчуванням",
|
||||
|
@ -190,7 +190,7 @@
|
|||
"a-name-is-required": "Необхідно вказати назву",
|
||||
"delete-with-name": "Видалити {name}",
|
||||
"confirm-delete-generic-with-name": "Ви дійсно хочете видалити {name}?",
|
||||
"confirm-delete-own-admin-account": "Зверніть увагу, що ви намагаєтеся видалити свій обліковий запис адміністратора! Цю дію неможливо скасувати і ви остаточно видалите ваш обліковий запис?",
|
||||
"confirm-delete-own-admin-account": "Зверніть увагу, що ви намагаєтеся видалити свій обліковий запис адміністратора! Цю дію неможливо скасувати й ви остаточно видалите ваш обліковий запис?",
|
||||
"organizer": "Організатор",
|
||||
"transfer": "Передача",
|
||||
"copy": "Скопіювати",
|
||||
|
@ -200,7 +200,7 @@
|
|||
"learn-more": "Дізнатися більше",
|
||||
"this-feature-is-currently-inactive": "Ця функція наразі не активна",
|
||||
"clipboard-not-supported": "Буфер обміну не підтримується",
|
||||
"copied-to-clipboard": "Скопійовано до буферу обміну",
|
||||
"copied-to-clipboard": "Скопійовано до буфера обміну",
|
||||
"your-browser-does-not-support-clipboard": "Ваш браузер не підтримує буфер обміну",
|
||||
"copied-items-to-clipboard": "Жоден елемент не скопійовано в буфер обміну|Один елемент скопійовано в буфер обміну|Скопійовано {count} елементів в буфер обміну",
|
||||
"actions": "Дії",
|
||||
|
@ -246,14 +246,14 @@
|
|||
"manage-members": "Керування Користувачами",
|
||||
"manage-members-description": "Керуйте дозволами учасників вашої сімʼї. {manage} дозволяє користувачеві отримати доступ до сторінки керування даними {invite} дозволяє користувачеві генерувати посилання запрошення для інших користувачів. Власники групи не можуть змінити власні дозволи.",
|
||||
"manage": "Керування",
|
||||
"manage-household": "Manage Household",
|
||||
"manage-household": "Керувати сімʼєю",
|
||||
"invite": "Запрошення",
|
||||
"looking-to-update-your-profile": "Бажаєте оновити свій профіль?",
|
||||
"default-recipe-preferences-description": "Це типові налаштування, коли створюється новий рецепт у вашій групі. Ці параметри можна змінити для окремих рецептів в меню налаштувань рецептів.",
|
||||
"default-recipe-preferences": "Параметри за умовчанням",
|
||||
"group-preferences": "Налаштування групи",
|
||||
"private-group": "Приватна група",
|
||||
"private-group-description": "Setting your group to private will disable all public view options. This overrides any individual public view settings",
|
||||
"private-group-description": "Якщо зробити групу приватною, то всі налаштування публічного перегляду буде скинуто. Це замінить індивідуальні налаштування публічного перегляду",
|
||||
"enable-public-access": "Дозволити загальний доступ",
|
||||
"enable-public-access-description": "Робить групові рецепти загальнодоступними за замовчуванням і дозволяє користувачам переглядати рецепти без входу в систему",
|
||||
"allow-users-outside-of-your-group-to-see-your-recipes": "Дозволити користувачам за межами вашої групи бачити ваші рецепти",
|
||||
|
@ -277,7 +277,7 @@
|
|||
"admin-group-management-text": "Зміни до цієї групи будуть відображені негайно.",
|
||||
"group-id-value": "Id групи: {0}",
|
||||
"total-households": "Всього сімей",
|
||||
"you-must-select-a-group-before-selecting-a-household": "You must select a group before selecting a household"
|
||||
"you-must-select-a-group-before-selecting-a-household": "Ви маєте вибрати групу перед тим, як вибирати сім'ю"
|
||||
},
|
||||
"household": {
|
||||
"household": "Сімʼя",
|
||||
|
@ -292,9 +292,9 @@
|
|||
"admin-household-management-text": "Зміни до цієї сімʼї будуть відображені негайно.",
|
||||
"household-id-value": "Ідентифікатор сімʼї: {0}",
|
||||
"private-household": "Приватна сімʼя",
|
||||
"private-household-description": "Setting your household to private will disable all public view options. This overrides any individual public view settings",
|
||||
"lock-recipe-edits-from-other-households": "Lock recipe edits from other households",
|
||||
"lock-recipe-edits-from-other-households-description": "When enabled only users in your household can edit recipes created by your household",
|
||||
"private-household-description": "Якщо зробити сім'ю приватною, то всі налаштування публічного перегляду буде скинуто. Це замінить індивідуальні налаштування публічного перегляду",
|
||||
"lock-recipe-edits-from-other-households": "Заблокувати редагування рецептів іншими сім'ями",
|
||||
"lock-recipe-edits-from-other-households-description": "Якщо увімкнено, тільки члени вашої сім'ї зможуть редагувати рецепти, які були створені вашою сім'єю",
|
||||
"household-recipe-preferences": "Налаштування рецептів сімʼї",
|
||||
"default-recipe-preferences-description": "Це типові налаштування для нового рецепта у вашій сімʼї. Ці параметри можна змінити для окремих рецептів в меню налаштувань рецептів.",
|
||||
"allow-users-outside-of-your-household-to-see-your-recipes": "Дозволити користувачам за межами вашої сімʼї бачити ваші рецепти",
|
||||
|
@ -321,10 +321,10 @@
|
|||
"mealplan-settings": "Налаштування плану харчування",
|
||||
"mealplan-update-failed": "Не вдалося оновити план харчування",
|
||||
"mealplan-updated": "План харчування оновлено",
|
||||
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
|
||||
"mealplan-households-description": "Якщо жодної сім'ї не вибрано, рецепти можуть бути доданими з будь-якої сім'ї",
|
||||
"any-category": "Будь-яка категорія",
|
||||
"any-tag": "Будь-який тег",
|
||||
"any-household": "Any Household",
|
||||
"any-household": "Будь-яка сім'я",
|
||||
"no-meal-plan-defined-yet": "Не створено жодного плану харчування",
|
||||
"no-meal-planned-for-today": "Не заплановано харчування на сьогодні",
|
||||
"numberOfDays-hint": "Скільки днів завантажувати на сторінку",
|
||||
|
@ -357,7 +357,7 @@
|
|||
"for-type-meal-types": "для {0} типів харчування",
|
||||
"meal-plan-rules": "Правила планів харчування",
|
||||
"new-rule": "Нове правило",
|
||||
"meal-plan-rules-description": "You can create rules for auto selecting recipes for your meal plans. These rules are used by the server to determine the random pool of recipes to select from when creating meal plans. Note that if rules have the same day/type constraints then the rule filters will be merged. In practice, it's unnecessary to create duplicate rules, but it's possible to do so.",
|
||||
"meal-plan-rules-description": "Ви можете створити правила для автоматичного вибору рецептів для ваших планів харчування. Ці правила використовуються сервером для вибору рецептів при створенні плану харчування. Зверніть увагу, що якщо правила мають обмеження на день/тип, то їх категорії будуть об'єднані. Дублювати правила немає сенсу, але можливо.",
|
||||
"new-rule-description": "При створенні нового правила для плану харчування, ви можете обмежити правило на певний день тижня та/або певний тип їжі. Щоб застосувати правило до всіх днів або всіх типів їжі, ви можете встановити правило \"Будь-який\", що застосовуватиме його до всіх можливих значень для дня та/або типу їжі.",
|
||||
"recipe-rules": "Правила рецептів",
|
||||
"applies-to-all-days": "Застосовується до всіх днів",
|
||||
|
@ -518,7 +518,7 @@
|
|||
"save-recipe-before-use": "Зберегти рецепт перед використанням",
|
||||
"section-title": "Назва розділу",
|
||||
"servings": "Порції",
|
||||
"serves-amount": "Serves {amount}",
|
||||
"serves-amount": "Порцій: {amount}",
|
||||
"share-recipe-message": "Я хотів би поділитися з тобою своїм рецептом {0}.",
|
||||
"show-nutrition-values": "Показати харчову цінність",
|
||||
"sodium-content": "Натрій",
|
||||
|
@ -547,8 +547,8 @@
|
|||
"failed-to-add-recipe-to-mealplan": "Не вдалося додати рецепт до плану харчування",
|
||||
"failed-to-add-to-list": "Не вдалося додати до списку",
|
||||
"yield": "Вихід",
|
||||
"yields-amount-with-text": "Yields {amount} {text}",
|
||||
"yield-text": "Yield Text",
|
||||
"yields-amount-with-text": "Вийде: {amount} {text}",
|
||||
"yield-text": "Текст виходу",
|
||||
"quantity": "Кількість",
|
||||
"choose-unit": "Виберіть одиниці вимірювання",
|
||||
"press-enter-to-create": "Натисніть Enter, щоб створити",
|
||||
|
@ -662,24 +662,24 @@
|
|||
"no-food": "Немає їжі"
|
||||
},
|
||||
"reset-servings-count": "Скинути кількість порцій",
|
||||
"not-linked-ingredients": "Additional Ingredients"
|
||||
"not-linked-ingredients": "Додаткові продукти"
|
||||
},
|
||||
"recipe-finder": {
|
||||
"recipe-finder": "Recipe Finder",
|
||||
"recipe-finder-description": "Search for recipes based on ingredients you have on hand. You can also filter by tools you have available, and set a maximum number of missing ingredients or tools.",
|
||||
"selected-ingredients": "Selected Ingredients",
|
||||
"no-ingredients-selected": "No ingredients selected",
|
||||
"missing": "Missing",
|
||||
"no-recipes-found": "No recipes found",
|
||||
"no-recipes-found-description": "Try adding more ingredients to your search or adjusting your filters",
|
||||
"include-ingredients-on-hand": "Include Ingredients On Hand",
|
||||
"include-tools-on-hand": "Include Tools On Hand",
|
||||
"max-missing-ingredients": "Max Missing Ingredients",
|
||||
"max-missing-tools": "Max Missing Tools",
|
||||
"selected-tools": "Selected Tools",
|
||||
"other-filters": "Other Filters",
|
||||
"ready-to-make": "Ready to Make",
|
||||
"almost-ready-to-make": "Almost Ready to Make"
|
||||
"recipe-finder": "Шукач рецептів",
|
||||
"recipe-finder-description": "Пошук рецептів базується на продуктах, які ви маєте. Ви також можете фільтрувати за наявними інструментами та встановити максимальну кількість відсутніх продуктів або інструментів.",
|
||||
"selected-ingredients": "Вибрані продукти",
|
||||
"no-ingredients-selected": "Жодного продукту не вибрано",
|
||||
"missing": "Відсутні",
|
||||
"no-recipes-found": "Рецептів не знайдено",
|
||||
"no-recipes-found-description": "Спробуйте додати більше продуктів до пошукового списку або підлаштувати фільтри",
|
||||
"include-ingredients-on-hand": "Включити наявні продукти",
|
||||
"include-tools-on-hand": "Включити наявні інструменти",
|
||||
"max-missing-ingredients": "Максимум відсутніх продуктів",
|
||||
"max-missing-tools": "Максимум відсутніх інструментів",
|
||||
"selected-tools": "Вибрані інструменти",
|
||||
"other-filters": "Інші фільтри",
|
||||
"ready-to-make": "Готове до приготування",
|
||||
"almost-ready-to-make": "Майже готове до приготування"
|
||||
},
|
||||
"search": {
|
||||
"advanced-search": "Розширений пошук",
|
||||
|
@ -884,7 +884,7 @@
|
|||
"are-you-sure-you-want-to-check-all-items": "Ви впевнені, що хочете відмітити всі елементи?",
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Ви впевнені, що хочете зняти відмітку з усіх елементів?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Ви впевнені, що хочете видалити всі відмічені елементи?",
|
||||
"no-shopping-lists-found": "No Shopping Lists Found"
|
||||
"no-shopping-lists-found": "Списків покупок не знайдено"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Всі рецепти",
|
||||
|
@ -1030,7 +1030,7 @@
|
|||
"administrator": "Адміністратор",
|
||||
"user-can-invite-other-to-group": "Користувач може запрошувати інших в групу",
|
||||
"user-can-manage-group": "Користувач може керувати групою",
|
||||
"user-can-manage-household": "User can manage household",
|
||||
"user-can-manage-household": "Користувач може управляти сім'єю",
|
||||
"user-can-organize-group-data": "Користувач може впорядковувати дані групи",
|
||||
"enable-advanced-features": "Увімкнути додаткові функції",
|
||||
"it-looks-like-this-is-your-first-time-logging-in": "Схоже, ви заходите вперше.",
|
||||
|
@ -1290,13 +1290,13 @@
|
|||
"debug-openai-services-description": "Використовуйте цю сторінку, щоб налагодити служби OpenAI. Ви можете перевірити ваше з'єднання з OpenAI й побачити результати тут. Якщо ввімкнено служби зображень, ви також можете надати зображення.",
|
||||
"run-test": "Запустити перевірку",
|
||||
"test-results": "Результати перевірки",
|
||||
"group-delete-note": "Groups with users or households cannot be deleted",
|
||||
"household-delete-note": "Households with users cannot be deleted"
|
||||
"group-delete-note": "Не можна видалити групи з користувачами чи сім'ями в ній",
|
||||
"household-delete-note": "Не можна видалити сім'ю з користувачами в ній"
|
||||
},
|
||||
"profile": {
|
||||
"welcome-user": "👋 Ласкаво просимо, {0}!",
|
||||
"description": "Керування вашим профілем, рецептами та налаштуваннями групи.",
|
||||
"invite-link": "Invite Link",
|
||||
"invite-link": "Посилання-запрошення",
|
||||
"get-invite-link": "Отримати посилання-запрошення",
|
||||
"get-public-link": "Отримати публічне посилання",
|
||||
"account-summary": "Аккаунт",
|
||||
|
@ -1345,9 +1345,9 @@
|
|||
},
|
||||
"cookbook": {
|
||||
"cookbooks": "Кулінарні книги",
|
||||
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
|
||||
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
|
||||
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
|
||||
"description": "Кулінарні книги - це ще один спосіб організовувати рецепти за допомогою розділів та інших фільтрів. Нова кулінарна книга з'явиться на боковій панелі, і всі рецепти, які відповідають обраним фільтрам, будуть показуватися в кулінарній книзі.",
|
||||
"hide-cookbooks-from-other-households": "Приховати кулінарні книги від інших сімей",
|
||||
"hide-cookbooks-from-other-households-description": "Якщо вибрано, тільки кулінарні книги вашої сім'ї буде видно на боковій панелі",
|
||||
"public-cookbook": "Публічна кулінарна книга",
|
||||
"public-cookbook-description": "Публічними кулінарними книгами можна поділитися з будь-ким, і вони будуть відображатися на сторінці вашої групи.",
|
||||
"filter-options": "Параметри фільтра",
|
||||
|
|
|
@ -151,6 +151,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Item -->
|
||||
<div v-if="createEditorOpen">
|
||||
<ShoppingListItemEditor
|
||||
v-model="createListItemData"
|
||||
class="my-4"
|
||||
:labels="allLabels || []"
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
:allow-delete="false"
|
||||
@delete="createEditorOpen = false"
|
||||
@cancel="createEditorOpen = false"
|
||||
@save="createListItem"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="d-flex justify-end">
|
||||
<BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- Reorder Labels -->
|
||||
<BaseDialog
|
||||
v-model="reorderLabelsDialog"
|
||||
|
@ -177,23 +195,6 @@
|
|||
</v-card>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Create Item -->
|
||||
<div v-if="createEditorOpen">
|
||||
<ShoppingListItemEditor
|
||||
v-model="createListItemData"
|
||||
class="my-4"
|
||||
:labels="allLabels || []"
|
||||
:units="allUnits || []"
|
||||
:foods="allFoods || []"
|
||||
@delete="createEditorOpen = false"
|
||||
@cancel="createEditorOpen = false"
|
||||
@save="createListItem"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="mt-4 d-flex justify-end">
|
||||
<BaseButton create @click="createEditorOpen = true" > {{ $t('general.add') }} </BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- Checked Items -->
|
||||
<div v-if="listItems.checked && listItems.checked.length > 0" class="mt-6">
|
||||
<div class="d-flex">
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
"""remove instructions index
|
||||
|
||||
Revision ID: 7cf3054cbbcc
|
||||
Revises: b9e516e2d3b3
|
||||
Create Date: 2025-02-09 15:31:00.772295
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.core.root_logger import get_logger
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7cf3054cbbcc"
|
||||
down_revision: str | None = "b9e516e2d3b3"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class SqlAlchemyBase(orm.DeclarativeBase):
|
||||
@classmethod
|
||||
def normalized_fields(cls) -> list[orm.InstrumentedAttribute]:
|
||||
return []
|
||||
|
||||
|
||||
class RecipeModel(SqlAlchemyBase):
|
||||
__tablename__ = "recipes"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name_normalized: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [cls.name_normalized, cls.description_normalized]
|
||||
|
||||
|
||||
class RecipeIngredientModel(SqlAlchemyBase):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
|
||||
id: orm.Mapped[int] = orm.mapped_column(sa.Integer, primary_key=True)
|
||||
note_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
original_text_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [cls.note_normalized, cls.original_text_normalized]
|
||||
|
||||
|
||||
class IngredientFoodModel(SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_foods"
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [cls.name_normalized, cls.plural_name_normalized]
|
||||
|
||||
|
||||
class IngredientFoodAliasModel(SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_foods_aliases"
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [cls.name_normalized]
|
||||
|
||||
|
||||
class IngredientUnitModel(SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_units"
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
abbreviation_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
plural_abbreviation_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [
|
||||
cls.name_normalized,
|
||||
cls.plural_name_normalized,
|
||||
cls.abbreviation_normalized,
|
||||
cls.plural_abbreviation_normalized,
|
||||
]
|
||||
|
||||
|
||||
class IngredientUnitAliasModel(SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_units_aliases"
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name_normalized: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
|
||||
|
||||
@classmethod
|
||||
def normalized_fields(cls):
|
||||
return [cls.name_normalized]
|
||||
|
||||
|
||||
def truncate_normalized_fields() -> None:
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
models: list[type[SqlAlchemyBase]] = [
|
||||
RecipeModel,
|
||||
RecipeIngredientModel,
|
||||
IngredientFoodModel,
|
||||
IngredientFoodAliasModel,
|
||||
IngredientUnitModel,
|
||||
IngredientUnitAliasModel,
|
||||
]
|
||||
|
||||
for model in models:
|
||||
for record in session.query(model).all():
|
||||
for field in model.normalized_fields():
|
||||
if not (field_value := getattr(record, field.key)):
|
||||
continue
|
||||
|
||||
setattr(record, field.key, field_value[:255])
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
logger.exception(f"Failed to truncate normalized fields for {model.__name__}")
|
||||
session.rollback()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_recipe_instructions_text")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
truncate_normalized_fields()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_recipe_instructions_text", ["text"], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,3 +1,12 @@
|
|||
import re
|
||||
import warnings
|
||||
|
||||
# pyrdfa3 is no longer being updated and has docstrings that emit syntax warnings
|
||||
warnings.filterwarnings(
|
||||
"ignore", module=".*pyRdfa", category=SyntaxWarning, message=re.escape("invalid escape sequence '\\-'")
|
||||
)
|
||||
|
||||
# ruff: noqa: E402
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||
from mealie.core.settings.themes import Theme
|
||||
|
||||
from .db_providers import AbstractDBProvider, db_provider_factory
|
||||
from .static import PACKAGE_DIR
|
||||
|
||||
|
||||
class ScheduleTime(NamedTuple):
|
||||
|
@ -109,7 +110,7 @@ class AppSettings(AppLoggingSettings):
|
|||
BASE_URL: str = "http://localhost:8080"
|
||||
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
|
||||
|
||||
STATIC_FILES: str = ""
|
||||
STATIC_FILES: str = str(PACKAGE_DIR / "frontend")
|
||||
"""path to static files directory (ex. `mealie/dist`)"""
|
||||
|
||||
IS_DEMO: bool = False
|
||||
|
|
|
@ -5,4 +5,5 @@ from mealie import __version__
|
|||
APP_VERSION = __version__
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
PACKAGE_DIR = CWD.parent.parent
|
||||
BASE_DIR = CWD.parent.parent.parent
|
||||
|
|
|
@ -69,16 +69,16 @@ def db_is_at_head(alembic_cfg: config.Config) -> bool:
|
|||
def safe_try(func: Callable):
|
||||
try:
|
||||
func()
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling '{func.__name__}': {e}")
|
||||
except Exception:
|
||||
logger.exception(f"Error calling '{func.__name__}'")
|
||||
|
||||
|
||||
def connect(session: orm.Session) -> bool:
|
||||
try:
|
||||
session.execute(text("SELECT 1"))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to database: {e}")
|
||||
except Exception:
|
||||
logger.exception("Error connecting to database")
|
||||
return False
|
||||
|
||||
|
||||
|
@ -106,23 +106,27 @@ def main():
|
|||
if not os.path.isfile(alembic_cfg_path):
|
||||
raise Exception("Provided alembic config path doesn't exist")
|
||||
|
||||
run_fixes = False
|
||||
alembic_cfg = Config(alembic_cfg_path)
|
||||
if db_is_at_head(alembic_cfg):
|
||||
logger.debug("Migration not needed.")
|
||||
else:
|
||||
logger.info("Migration needed. Performing migration...")
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
run_fixes = True
|
||||
|
||||
if session.get_bind().name == "postgresql": # needed for fuzzy search and fast GIN text indices
|
||||
session.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm;"))
|
||||
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
safe_try(lambda: fix_migration_data(session))
|
||||
safe_try(lambda: fix_slug_food_names(db))
|
||||
safe_try(lambda: fix_group_with_no_name(session))
|
||||
|
||||
if db.users.get_all():
|
||||
logger.debug("Database exists")
|
||||
if run_fixes:
|
||||
safe_try(lambda: fix_migration_data(session))
|
||||
safe_try(lambda: fix_slug_food_names(db))
|
||||
safe_try(lambda: fix_group_with_no_name(session))
|
||||
|
||||
else:
|
||||
logger.info("Database contains no users, initializing...")
|
||||
init_db(session)
|
||||
|
|
|
@ -18,7 +18,9 @@ class SqlAlchemyBase(DeclarativeBase):
|
|||
|
||||
@classmethod
|
||||
def normalize(cls, val: str) -> str:
|
||||
return unidecode(val).lower().strip()
|
||||
# We cap the length to 255 to prevent indexes from being too long; see:
|
||||
# https://www.postgresql.org/docs/current/btree.html
|
||||
return unidecode(val).lower().strip()[:255]
|
||||
|
||||
|
||||
class BaseMixins:
|
||||
|
|
|
@ -23,8 +23,8 @@ class RecipeInstruction(SqlAlchemyBase):
|
|||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
position: Mapped[int | None] = mapped_column(Integer, index=True)
|
||||
type: Mapped[str | None] = mapped_column(String, default="")
|
||||
title: Mapped[str | None] = mapped_column(String) # This is the section title!!!
|
||||
text: Mapped[str | None] = mapped_column(String, index=True)
|
||||
title: Mapped[str | None] = mapped_column(String) # This is the section title
|
||||
text: Mapped[str | None] = mapped_column(String)
|
||||
summary: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"generic": {
|
||||
"server-error": "An unexpected error occurred"
|
||||
"server-error": "Ocorreu un erro inesperado"
|
||||
},
|
||||
"recipe": {
|
||||
"unique-name-error": "Recipe names must be unique",
|
||||
"recipe-created": "Recipe Created",
|
||||
"unique-name-error": "Os nomes de receitas deven ser únicos",
|
||||
"recipe-created": "Receita creada",
|
||||
"recipe-defaults": {
|
||||
"ingredient-note": "1 Cup Flour",
|
||||
"step-text": "Recipe steps as well as other fields in the recipe page support markdown syntax.\n\n**Add a link**\n\n[My Link](https://demo.mealie.io)\n"
|
||||
"step-text": "Os pasos da receita, como outros campos na páxina da receita, suportan a sintaxe markdown.\n\n**Adicionar un link**\n\n[Meu link](https://demo.mealie.io)\n"
|
||||
},
|
||||
"servings-text": {
|
||||
"makes": "Makes",
|
||||
|
@ -33,26 +33,26 @@
|
|||
"exceptions": {
|
||||
"permission_denied": "You do not have permission to perform this action",
|
||||
"no-entry-found": "The requested resource was not found",
|
||||
"integrity-error": "Database integrity error",
|
||||
"username-conflict-error": "This username is already taken",
|
||||
"integrity-error": "Erro de integridade da base de datos",
|
||||
"username-conflict-error": "Este nome de usuario xa está en uso",
|
||||
"email-conflict-error": "This email is already in use"
|
||||
},
|
||||
"notifications": {
|
||||
"generic-created": "{name} was created",
|
||||
"generic-updated": "{name} was updated",
|
||||
"generic-created-with-url": "{name} has been created, {url}",
|
||||
"generic-updated-with-url": "{name} has been updated, {url}",
|
||||
"generic-duplicated": "{name} has been duplicated",
|
||||
"generic-created": "{name} creado",
|
||||
"generic-updated": "{name} atualizado",
|
||||
"generic-created-with-url": "{name} foi creado, {url}",
|
||||
"generic-updated-with-url": "{name} foi atualizado, {url}",
|
||||
"generic-duplicated": "{name} foi duplicado",
|
||||
"generic-deleted": "{name} has been deleted"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
"year": "ano|anos",
|
||||
"day": "dia|dias",
|
||||
"hour": "hora|horas",
|
||||
"minute": "minuto|minutos",
|
||||
"second": "segundo|segundos",
|
||||
"millisecond": "milisegundo|milisegundos",
|
||||
"microsecond": "microsegundo|microsegundos"
|
||||
},
|
||||
"emails": {
|
||||
"password": {
|
||||
|
@ -67,14 +67,14 @@
|
|||
"header_text": "You're Invited!",
|
||||
"message_top": "You have been invited to join Mealie.",
|
||||
"message_bottom": "Please click the button above to accept the invitation.",
|
||||
"button_text": "Accept Invitation"
|
||||
"button_text": "Aceptar Convite"
|
||||
},
|
||||
"test": {
|
||||
"subject": "Mealie Test Email",
|
||||
"header_text": "Test Email",
|
||||
"message_top": "This is a test email.",
|
||||
"message_bottom": "Please click the button above to test the email.",
|
||||
"button_text": "Open Mealie"
|
||||
"button_text": "Abrir o Mealie"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
},
|
||||
"recipe": {
|
||||
"unique-name-error": "Назва рецепту повинна бути унікальною",
|
||||
"recipe-created": "Recipe Created",
|
||||
"recipe-created": "Рецепт створено",
|
||||
"recipe-defaults": {
|
||||
"ingredient-note": "Стакан борошна",
|
||||
"step-text": "Кроки рецептів, так само як і інші поля сторінки, підтримують синтаксис markdown.\n\n**Додати посилання**\n\n[Mоє посилання](https://demo.mealie.io)\n"
|
||||
"step-text": "Кроки рецептів, так само як і інші поля сторінки, підтримують синтаксис markdown.\n\n**Додати посилання**\n\n[Моє посилання](https://demo.mealie.io)\n"
|
||||
},
|
||||
"servings-text": {
|
||||
"makes": "Makes",
|
||||
"serves": "Serves",
|
||||
"serving": "Serving",
|
||||
"servings": "Servings",
|
||||
"serving": "Порція",
|
||||
"servings": "Порції",
|
||||
"yield": "Yield",
|
||||
"yields": "Yields"
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ from mealie.core.logger.config import log_config
|
|||
|
||||
def main():
|
||||
uvicorn.run(
|
||||
"app:app",
|
||||
"mealie.app:app",
|
||||
host=settings.API_HOST,
|
||||
port=settings.API_PORT,
|
||||
log_level=settings.LOG_LEVEL.lower(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"acorn-squash": {
|
||||
"name": "courgeron"
|
||||
"name": "courge poivrée"
|
||||
},
|
||||
"alfalfa-sprouts": {
|
||||
"name": "pousses de luzerne"
|
||||
|
@ -109,7 +109,7 @@
|
|||
"name": "cannabis"
|
||||
},
|
||||
"capsicum": {
|
||||
"name": "capsicum"
|
||||
"name": "piment"
|
||||
},
|
||||
"caraway": {
|
||||
"name": "cumin"
|
||||
|
|
|
@ -6,17 +6,17 @@
|
|||
"name": "alfalfa sprouts"
|
||||
},
|
||||
"anchovies": {
|
||||
"name": "anchovies"
|
||||
"name": "anchoas"
|
||||
},
|
||||
"apples": {
|
||||
"name": "apple",
|
||||
"plural_name": "apples"
|
||||
"name": "mazá",
|
||||
"plural_name": "mazás"
|
||||
},
|
||||
"artichoke": {
|
||||
"name": "artichoke"
|
||||
"name": "alcachofa"
|
||||
},
|
||||
"arugula": {
|
||||
"name": "arugula"
|
||||
"name": "rúcula"
|
||||
},
|
||||
"asparagus": {
|
||||
"name": "asparagus"
|
||||
|
@ -84,7 +84,7 @@
|
|||
"name": "brussels sprouts"
|
||||
},
|
||||
"butter": {
|
||||
"name": "butter"
|
||||
"name": "manteiga"
|
||||
},
|
||||
"butternut-pumpkin": {
|
||||
"name": "butternut pumpkin"
|
||||
|
@ -115,8 +115,8 @@
|
|||
"name": "caraway"
|
||||
},
|
||||
"carrot": {
|
||||
"name": "carrot",
|
||||
"plural_name": "carrots"
|
||||
"name": "cenoura",
|
||||
"plural_name": "cenouras"
|
||||
},
|
||||
"caster-sugar": {
|
||||
"name": "caster sugar"
|
||||
|
@ -176,7 +176,7 @@
|
|||
},
|
||||
"coconut": {
|
||||
"name": "coco",
|
||||
"plural_name": "coconuts"
|
||||
"plural_name": "cocos"
|
||||
},
|
||||
"coconut-milk": {
|
||||
"name": "leite de coco"
|
||||
|
@ -194,7 +194,7 @@
|
|||
"name": "confectioners' sugar"
|
||||
},
|
||||
"coriander": {
|
||||
"name": "coriander"
|
||||
"name": "coentro"
|
||||
},
|
||||
"corn": {
|
||||
"name": "millo",
|
||||
|
@ -213,7 +213,7 @@
|
|||
"name": "cream of tartar"
|
||||
},
|
||||
"cucumber": {
|
||||
"name": "pepino",
|
||||
"name": "cogombro",
|
||||
"plural_name": "cucumbers"
|
||||
},
|
||||
"cumin": {
|
||||
|
@ -243,8 +243,8 @@
|
|||
"plural_name": "eggplants"
|
||||
},
|
||||
"eggs": {
|
||||
"name": "ovos",
|
||||
"plural_name": "eggs"
|
||||
"name": "ovo",
|
||||
"plural_name": "ovos"
|
||||
},
|
||||
"endive": {
|
||||
"name": "endive",
|
||||
|
@ -291,14 +291,14 @@
|
|||
"name": "garam masala"
|
||||
},
|
||||
"garlic": {
|
||||
"name": "garlic",
|
||||
"plural_name": "garlics"
|
||||
"name": "allo",
|
||||
"plural_name": "allos"
|
||||
},
|
||||
"gem-squash": {
|
||||
"name": "gem squash"
|
||||
},
|
||||
"ghee": {
|
||||
"name": "ghee"
|
||||
"name": "manteiga ghee"
|
||||
},
|
||||
"giblets": {
|
||||
"name": "giblets"
|
||||
|
@ -330,7 +330,7 @@
|
|||
"name": "herbs"
|
||||
},
|
||||
"honey": {
|
||||
"name": "honey"
|
||||
"name": "mel"
|
||||
},
|
||||
"isomalt": {
|
||||
"name": "isomalt"
|
||||
|
@ -393,7 +393,7 @@
|
|||
"name": "maple syrup"
|
||||
},
|
||||
"meat": {
|
||||
"name": "meat"
|
||||
"name": "carne"
|
||||
},
|
||||
"milk": {
|
||||
"name": "leite"
|
||||
|
@ -440,7 +440,7 @@
|
|||
"name": "olive oil"
|
||||
},
|
||||
"onion": {
|
||||
"name": "onion"
|
||||
"name": "cebola"
|
||||
},
|
||||
"onion-family": {
|
||||
"name": "onion family"
|
||||
|
@ -524,7 +524,7 @@
|
|||
"name": "arroz"
|
||||
},
|
||||
"rice-flour": {
|
||||
"name": "rice flour"
|
||||
"name": "fariña de arroz"
|
||||
},
|
||||
"rock-sugar": {
|
||||
"name": "rock sugar"
|
||||
|
@ -536,7 +536,7 @@
|
|||
"name": "salmón"
|
||||
},
|
||||
"salt": {
|
||||
"name": "salt"
|
||||
"name": "sal"
|
||||
},
|
||||
"salt-cod": {
|
||||
"name": "bacallau en salgadura"
|
||||
|
@ -546,7 +546,7 @@
|
|||
"plural_name": "scallions"
|
||||
},
|
||||
"seafood": {
|
||||
"name": "seafood"
|
||||
"name": "marisco"
|
||||
},
|
||||
"seeds": {
|
||||
"name": "seeds"
|
||||
|
@ -633,7 +633,7 @@
|
|||
},
|
||||
"tomato": {
|
||||
"name": "tomate",
|
||||
"plural_name": "tomatoes"
|
||||
"plural_name": "tomates"
|
||||
},
|
||||
"trout": {
|
||||
"name": "troita"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"plural_name": "abacate"
|
||||
},
|
||||
"bacon": {
|
||||
"name": "bacon"
|
||||
"name": "Carne fumada"
|
||||
},
|
||||
"baking-powder": {
|
||||
"name": "fermento em pó"
|
||||
|
@ -109,7 +109,7 @@
|
|||
"name": "canábis"
|
||||
},
|
||||
"capsicum": {
|
||||
"name": "capsicum"
|
||||
"name": "pimentão"
|
||||
},
|
||||
"caraway": {
|
||||
"name": "alcarávia"
|
||||
|
|
|
@ -361,7 +361,7 @@
|
|||
"name": "kolerabe"
|
||||
},
|
||||
"kumara": {
|
||||
"name": "kumara"
|
||||
"name": "sladek krompir"
|
||||
},
|
||||
"leavening-agents": {
|
||||
"name": "kvas"
|
||||
|
@ -377,7 +377,7 @@
|
|||
"name": "limonina trava"
|
||||
},
|
||||
"lentils": {
|
||||
"name": "lentils"
|
||||
"name": "leča"
|
||||
},
|
||||
"lettuce": {
|
||||
"name": "solata"
|
||||
|
@ -434,7 +434,7 @@
|
|||
"name": "jedilni oslez"
|
||||
},
|
||||
"olive": {
|
||||
"name": "olive"
|
||||
"name": "oliva"
|
||||
},
|
||||
"olive-oil": {
|
||||
"name": "olivno olje"
|
||||
|
|
|
@ -616,7 +616,7 @@
|
|||
},
|
||||
"sweetcorn": {
|
||||
"name": "sockermajs",
|
||||
"plural_name": "sweetcorns"
|
||||
"plural_name": "majs"
|
||||
},
|
||||
"sweeteners": {
|
||||
"name": "sötningsmedel"
|
||||
|
@ -680,7 +680,7 @@
|
|||
},
|
||||
"yam": {
|
||||
"name": "jams",
|
||||
"plural_name": "yams"
|
||||
"plural_name": "sötpotatisar"
|
||||
},
|
||||
"yeast": {
|
||||
"name": "jäst"
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
"name": "Drycker"
|
||||
},
|
||||
{
|
||||
"name": "Bakade varor"
|
||||
"name": "Bakverk"
|
||||
},
|
||||
{
|
||||
"name": "Konserverade varor"
|
||||
"name": "Konserver"
|
||||
},
|
||||
{
|
||||
"name": "Smaktillsatser"
|
||||
|
|
|
@ -91,8 +91,8 @@
|
|||
"abbreviation": ""
|
||||
},
|
||||
"dash": {
|
||||
"name": "1/8 de cuillère à café",
|
||||
"plural_name": "1/8 de cuillères à café",
|
||||
"name": "goutte",
|
||||
"plural_name": "gouttes",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
|
@ -103,8 +103,8 @@
|
|||
"abbreviation": ""
|
||||
},
|
||||
"head": {
|
||||
"name": "tête",
|
||||
"plural_name": "têtes",
|
||||
"name": "personne",
|
||||
"plural_name": "personnes",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
|
|
|
@ -109,14 +109,14 @@
|
|||
"abbreviation": ""
|
||||
},
|
||||
"clove": {
|
||||
"name": "clove",
|
||||
"plural_name": "cloves",
|
||||
"name": "dente",
|
||||
"plural_name": "dentes",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
"can": {
|
||||
"name": "can",
|
||||
"plural_name": "cans",
|
||||
"name": "lata",
|
||||
"plural_name": "latas",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
"quart": {
|
||||
"name": "quart",
|
||||
"plural_name": "quarts",
|
||||
"plural_name": "quart",
|
||||
"description": "",
|
||||
"abbreviation": "qt"
|
||||
},
|
||||
|
|
|
@ -86,13 +86,13 @@
|
|||
},
|
||||
"splash": {
|
||||
"name": "трохи",
|
||||
"plural_name": "splashes",
|
||||
"plural_name": "трохи",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
"dash": {
|
||||
"name": "трохи",
|
||||
"plural_name": "dashes",
|
||||
"plural_name": "трохи",
|
||||
"description": "",
|
||||
"abbreviation": ""
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
from textwrap import dedent
|
||||
|
||||
from openai import NOT_GIVEN, AsyncOpenAI
|
||||
from openai.resources.chat.completions import ChatCompletion
|
||||
from openai.types.chat import ChatCompletion
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
|
|
793
poetry.lock
generated
793
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,9 +4,13 @@ description = "A Recipe Manager"
|
|||
license = "AGPL"
|
||||
name = "mealie"
|
||||
version = "2.6.0"
|
||||
include = [
|
||||
# Explicit include to override .gitignore when packaging the frontend
|
||||
{ path = "mealie/frontend/**/*", format = ["sdist", "wheel"] }
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
start = "mealie.app:main"
|
||||
mealie = "mealie.main:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
Jinja2 = "^3.1.2"
|
||||
|
@ -47,7 +51,7 @@ paho-mqtt = "^1.6.1"
|
|||
pydantic-settings = "^2.1.0"
|
||||
pillow-heif = "^0.21.0"
|
||||
pyjwt = "^2.8.0"
|
||||
openai = "^1.27.0"
|
||||
openai = "^1.63.0"
|
||||
typing-extensions = "^4.12.2"
|
||||
itsdangerous = "^2.2.0"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue