Compare commits

...

12 Commits

Author SHA1 Message Date
renovate[bot]
ffeb4dceaf chore(deps): update dependency mypy to v1.20.1 (#7490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 05:55:14 +00:00
mealie-commit-bot[bot]
5fc4851ef5 chore: bump version to v3.16.0 2026-04-17 20:59:57 +00:00
Michael Genson
d9e933d5ae fix: Misc frontend layout fixes (#7487) 2026-04-17 12:28:13 -05:00
renovate[bot]
0a07835338 fix(deps): update dependency lxml to v6.0.4 (#7485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 16:46:53 +00:00
Michael Genson
7a85ea6ae9 chore: Update yarn deps (#7486) 2026-04-17 11:58:19 -05:00
renovate[bot]
c4c60f1645 fix(deps): update dependency authlib to v1.6.11 [security] (#7481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 16:46:22 +00:00
Zdenek Stursa
9f7ba8dc08 fix: preserve ingredient section titles when parsing recipe ingredients (#7483)
Co-authored-by: Zdenek <tvuj-email@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:44:45 +00:00
Michael Genson
c4799ceb9e dev: Enable lockfile maintenance and update deps (#7484) 2026-04-17 11:33:50 -05:00
Michael Genson
828be095a2 fix: Blank query filter builder fields (#7480) 2026-04-16 19:11:05 -05:00
renovate[bot]
18718fb647 chore(deps): update node.js to 33cf7f0 (#7478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 15:57:22 +00:00
Brian Choromanski
fb545962dd feat: Migrate PWA manifest to backend (#7331)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-04-16 10:59:21 -05:00
Brian Choromanski
781a08ef54 docs: Added copy button to codeblocks (#7343) 2026-04-16 15:34:28 +00:00
19 changed files with 2560 additions and 2926 deletions

View File

@@ -1,7 +1,7 @@
###############################################
# Frontend Build
###############################################
FROM node:24@sha256:80fc934952c8f1b2b4d39907af7211f8a9fff1a4c2cf673fb49099292c251cec \
FROM node:24@sha256:33cf7f057918860b043c307751ef621d74ac96f875b79b6724dcebf2dfd0db6d \
AS frontend-builder
WORKDIR /frontend

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.15.2`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.16.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.16.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.15.2 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.16.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -19,6 +19,7 @@ theme:
custom_dir: docs/overrides
features:
- content.code.annotate
- content.code.copy
- navigation.top
- navigation.instant
- navigation.expand

View File

@@ -20,16 +20,12 @@
max-width: 1100px !important;
}
.theme--dark.v-application {
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
.v-theme--dark.v-application {
background-color: rgb(var(--v-theme-background)) !important;
}
.theme--dark.v-navigation-drawer {
background-color: rgb(var(--v-theme-background, 30, 30, 30)) !important;
}
.theme--dark.v-card {
background-color: #1e1e1e !important;
.v-theme--dark .v-navigation-drawer {
background-color: rgb(var(--v-theme-background)) !important;
}
.left-border {

View File

@@ -41,19 +41,14 @@
>
<v-select
v-if="index"
:model-value="field.logicalOperator"
:model-value="field.logicalOperator?.value"
:items="[logOps.AND, logOps.OR]"
item-title="label"
item-value="value"
variant="underlined"
class="text-center"
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- left parenthesis -->
@@ -67,14 +62,9 @@
:model-value="field.leftParenthesis"
:items="['', '(', '((', '(((']"
variant="underlined"
class="text-center"
@update:model-value="setLeftParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- field name -->
@@ -84,19 +74,14 @@
:class="config.col.class"
>
<v-select
chips
:model-value="field.label"
:items="fieldDefs"
variant="underlined"
item-title="label"
item-value="label"
class="text-center"
@update:model-value="setField(index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- relational operator -->
@@ -107,19 +92,14 @@
>
<v-select
v-if="field.type !== 'boolean'"
:model-value="field.relationalOperatorValue"
:model-value="field.relationalOperatorValue?.value"
:items="field.relationalOperatorChoices"
item-title="label"
item-value="value"
variant="underlined"
class="text-center"
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
/>
</v-col>
<!-- field value -->
@@ -275,23 +255,14 @@
:model-value="field.rightParenthesis"
:items="['', ')', '))', ')))']"
variant="underlined"
class="text-center"
@update:model-value="setRightParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field actions -->
<v-col
/>
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
:cols="config.items.fieldActions.cols(index)"
:sm="config.items.fieldActions.sm(index)"
:class="config.col.class"
>
>
<BaseButtonGroup
:buttons="[
{
@@ -723,9 +694,6 @@ const config = computed(() => {
col: {
class: "d-flex justify-center align-end py-0",
},
select: {
textClass: "d-flex justify-center text-center",
},
items: {
icon: {
cols: (_index: number) => 2,

View File

@@ -371,14 +371,18 @@ async function parseIngredients() {
}
state.loading.parser = true;
try {
const ingsAsString = props.ingredients
.filter(ing => !ing.referencedRecipe)
.map(ing => ingredientToParserString(ing));
const filteredIngredients = props.ingredients.filter(ing => !ing.referencedRecipe);
const ingsAsString = filteredIngredients.map(ing => ingredientToParserString(ing));
const { data, error } = await api.recipes.parseIngredients(parser.value, ingsAsString);
if (error || !data) {
throw new Error("Failed to parse ingredients");
}
parsedIngs.value = data;
// Restore section titles from original ingredients — the parser doesn't return them
data.forEach((parsed, index) => {
parsed.ingredient.title = filteredIngredients[index]?.title || "";
});
const parsed = data ?? [];
const recipeRefs = props.ingredients.filter(ing => ing.referencedRecipe).map(ing => ({
input: ing.note || "",

View File

@@ -1,6 +1,6 @@
<template>
<div style="height: 100%;">
<v-row class="my-0 mx-7">
<v-row class="mb-0 mt-3 mx-7">
<v-spacer />
<v-col class="text-right">
<!-- Filters -->
@@ -44,6 +44,7 @@
:model-value="option.checked"
color="primary"
readonly
hide-details
@click="toggleEventTypeOption(option.value)"
>
<template #label>

View File

@@ -28,7 +28,6 @@
<v-col v-else cols="9" style="margin: auto; text-align: center">
{{ event.subject }}
</v-col>
<v-spacer />
<v-col :cols="useMobileFormat ? 'auto' : '1'" class="px-0 pt-0">
<RecipeTimelineContextMenu
v-if="currentUser && currentUser.id == event.userId && event.eventType != 'system'"

View File

@@ -68,6 +68,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
warning: theme?.darkWarning ?? "#FF6D00",
error: theme?.darkError ?? "#EF5350",
background: "#1E1E1E",
surface: "#1E1E1E",
},
},
},

View File

@@ -234,151 +234,7 @@ export default defineNuxtConfig({
periodicSyncForUpdates: 120,
},
includeAssets: ["favicon.ico", "apple-touch-icon.png", "safari-pinned-tab.svg"],
manifest: {
name: "Mealie",
short_name: "Mealie",
id: "/",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#FFFFFF",
theme_color: process.env.THEME_LIGHT_PRIMARY || "#E58325",
description: "Mealie is a recipe management and meal planning app",
lang: "en",
display_override: [
"standalone",
"minimal-ui",
"browser",
"window-controls-overlay",
],
categories: ["food", "lifestyle"],
prefer_related_applications: false,
handle_links: "preferred",
launch_handler: {
client_mode: ["focus-existing", "auto"],
},
edge_side_panel: {
preferred_width: 400,
},
share_target: {
action: "/r/create/url",
method: "GET",
enctype: "application/x-www-form-urlencoded",
params: {
text: "recipe_import_url",
},
},
icons: [
{
src: "/icons/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/icons/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/icons/android-chrome-maskable-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/icons/android-chrome-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
screenshots: [
{
src: "/screenshots/home-narrow.png",
sizes: "1600x2420",
form_factor: "narrow",
label: "Home Page",
},
{
src: "/screenshots/recipe-narrow.png",
sizes: "1600x2420",
form_factor: "narrow",
label: "Recipe Page",
},
{
src: "/screenshots/editor-narrow.png",
sizes: "1600x2420",
form_factor: "narrow",
label: "Editor Page",
},
{
src: "/screenshots/parser-narrow.png",
sizes: "1600x2420",
form_factor: "narrow",
label: "Parser Page",
},
{
src: "/screenshots/home-wide.png",
sizes: "2560x1460",
form_factor: "wide",
label: "Home Page",
},
{
src: "/screenshots/recipe-wide.png",
sizes: "2560x1460",
form_factor: "wide",
label: "Recipe Page",
},
{
src: "/screenshots/editor-wide.png",
sizes: "2560x1460",
form_factor: "wide",
label: "Editor Page",
},
{
src: "/screenshots/parser-wide.png",
sizes: "2560x1460",
form_factor: "wide",
label: "Parser Page",
},
],
shortcuts: [
{
name: "Shopping Lists",
short_name: "Shopping Lists",
description: "Open the shopping lists",
url: "/shopping-lists",
icons: [
{
src: "/icons/mdiFormatListChecks-192x192.png",
sizes: "192x192",
},
{
src: "/icons/mdiFormatListChecks-96x96.png",
sizes: "96x96",
},
],
},
{
name: "Meal Planner",
short_name: "Meal Planner",
description: "Open the meal planner",
url: "/household/mealplan/planner/view",
icons: [
{
src: "/icons/mdiCalendarMultiselect-192x192.png",
sizes: "192x192",
},
{
src: "/icons/mdiCalendarMultiselect-96x96.png",
sizes: "96x96",
},
],
},
],
},
manifest: false, // This is served via the backend, see mealie/routes/spa/manifest.py
},
// Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify

View File

@@ -1,6 +1,6 @@
{
"name": "mealie",
"version": "3.15.2",
"version": "3.16.0",
"private": true,
"scripts": {
"dev": "nuxt dev",
@@ -26,7 +26,7 @@
"axios": "^1.8.1",
"date-fns": "^4.1.0",
"fuse.js": "^7.1.0",
"isomorphic-dompurify": "^2.28.0",
"isomorphic-dompurify": "^3.4.0",
"json-editor-vue": "^0.18.1",
"marked": "^15.0.12",
"nuxt": "^4.4.2",
@@ -55,8 +55,6 @@
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"resolutions": {
"esbuild": ">=0.25.0",
"glob": ">=10.5.0",
"js-yaml": ">=4.1.1",
"node-forge": ">=1.3.2"
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ from mealie.core.config import get_app_settings
from mealie.core.dependencies.dependencies import try_get_current_user
from mealie.db.db_setup import generate_session
from mealie.repos.repository_factory import AllRepositories
from mealie.routes.spa.manifest import serve_manifest
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.user.user import PrivateUser
@@ -251,4 +252,5 @@ def mount_spa(app: FastAPI):
app.get("/g/{group_slug}/r/{recipe_slug}", include_in_schema=False)(serve_recipe_with_meta)
app.get("/g/{group_slug}/shared/r/{token_id}", include_in_schema=False)(serve_shared_recipe_with_meta)
app.get("/manifest.webmanifest", include_in_schema=False)(serve_manifest)
app.mount("/", SPAStaticFiles(directory=__app_settings.STATIC_FILES, html=True), name="spa")

View File

@@ -0,0 +1,125 @@
import json
from urllib.parse import urlparse
from fastapi import Response
from mealie.core.config import get_app_settings
def serve_manifest():
settings = get_app_settings()
sub_path = urlparse(settings.BASE_URL).path or "/"
manifest = {
"name": "Mealie",
"short_name": "Mealie",
"id": "/",
"start_url": sub_path,
"scope": sub_path,
"display": "standalone",
"background_color": "#1E1E1E",
"theme_color": settings.theme.light_primary,
"description": "Mealie is a recipe management and meal planning app",
"lang": "en",
"display_override": ["standalone", "minimal-ui", "browser", "window-controls-overlay"],
"categories": ["food", "lifestyle"],
"prefer_related_applications": False,
"handle_links": "preferred",
"launch_handler": {"client_mode": ["focus-existing", "auto"]},
"edge_side_panel": {"preferred_width": 400},
"share_target": {
"action": "/r/create/url",
"method": "GET",
"enctype": "application/x-www-form-urlencoded",
"params": {"text": "recipe_import_url"},
},
"icons": [
{"src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any"},
{"src": "/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any"},
{
"src": "/icons/android-chrome-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable",
},
{
"src": "/icons/android-chrome-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable",
},
],
"screenshots": [
{
"src": "/screenshots/home-narrow.png",
"sizes": "1600x2420",
"form_factor": "narrow",
"label": "Home Page",
},
{
"src": "/screenshots/recipe-narrow.png",
"sizes": "1600x2420",
"form_factor": "narrow",
"label": "Recipe Page",
},
{
"src": "/screenshots/editor-narrow.png",
"sizes": "1600x2420",
"form_factor": "narrow",
"label": "Editor Page",
},
{
"src": "/screenshots/parser-narrow.png",
"sizes": "1600x2420",
"form_factor": "narrow",
"label": "Parser Page",
},
{"src": "/screenshots/home-wide.png", "sizes": "2560x1460", "form_factor": "wide", "label": "Home Page"},
{
"src": "/screenshots/recipe-wide.png",
"sizes": "2560x1460",
"form_factor": "wide",
"label": "Recipe Page",
},
{
"src": "/screenshots/editor-wide.png",
"sizes": "2560x1460",
"form_factor": "wide",
"label": "Editor Page",
},
{
"src": "/screenshots/parser-wide.png",
"sizes": "2560x1460",
"form_factor": "wide",
"label": "Parser Page",
},
],
"shortcuts": [
{
"name": "Shopping Lists",
"short_name": "Shopping Lists",
"description": "Open the shopping lists",
"url": "/shopping-lists",
"icons": [
{"src": "/icons/mdiFormatListChecks-192x192.png", "sizes": "192x192"},
{"src": "/icons/mdiFormatListChecks-96x96.png", "sizes": "96x96"},
],
},
{
"name": "Meal Planner",
"short_name": "Meal Planner",
"description": "Open the meal planner",
"url": "/household/mealplan/planner/view",
"icons": [
{"src": "/icons/mdiCalendarMultiselect-192x192.png", "sizes": "192x192"},
{"src": "/icons/mdiCalendarMultiselect-96x96.png", "sizes": "96x96"},
],
},
],
}
return Response(
content=json.dumps(manifest),
media_type="application/manifest+json",
headers={"Cache-Control": "no-cache"},
)

View File

@@ -1,6 +1,6 @@
[project]
name = "mealie"
version = "3.15.2"
version = "3.16.0"
description = "A Recipe Manager"
authors = [{ name = "Hayden", email = "hay-kot@pm.me" }]
license = "AGPL-3.0-only"
@@ -19,7 +19,7 @@ dependencies = [
"extruct==0.18.0",
"fastapi==0.135.3",
"httpx==0.28.1",
"lxml==6.0.3",
"lxml==6.0.4",
"orjson==3.11.8",
"pydantic==2.12.5",
"pyhumps==3.8.0",
@@ -36,7 +36,7 @@ dependencies = [
"isodate==0.7.2",
"text-unidecode==1.3",
"rapidfuzz==3.14.5",
"authlib==1.6.9",
"authlib==1.6.11",
"html2text==2025.4.15",
"paho-mqtt==1.6.1",
"pydantic-settings==2.13.1",
@@ -67,7 +67,7 @@ dev = [
"coverage==7.13.5",
"coveragepy-lcov==0.1.2",
"mkdocs-material==9.7.6",
"mypy==1.20.0",
"mypy==1.20.1",
"pre-commit==4.5.1",
"pylint==4.0.5",
"pytest==9.0.3",

View File

@@ -1,5 +1,8 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"lockFileMaintenance": {
"enabled": true
},
"enabledManagers": [
"pep621",
"dockerfile",

630
uv.lock generated

File diff suppressed because it is too large Load Diff