{"id":46627414,"url":"https://github.com/featurecreep-cron/morsl","last_synced_at":"2026-04-01T19:19:26.903Z","repository":{"id":342891247,"uuid":"1175564402","full_name":"featurecreep-cron/morsl","owner":"featurecreep-cron","description":"Generate weekly menus from your Tandoor Recipes collection. Picks recipes based on your preferences, serves a household menu, syncs orders back as meal plans.","archived":false,"fork":false,"pushed_at":"2026-03-23T22:54:40.000Z","size":3889,"stargazers_count":2,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T11:34:36.315Z","etag":null,"topics":["docker","homelab","meal-planner","menu-generator","recipe","self-hosted","tandoor"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/featurecreep-cron.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"buy_me_a_coffee":"featurecreep"}},"created_at":"2026-03-07T22:00:53.000Z","updated_at":"2026-03-23T22:53:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/featurecreep-cron/morsl","commit_stats":null,"previous_names":["featurecreep-cron/morsl"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/featurecreep-cron/morsl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featurecreep-cron%2Fmorsl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featurecreep-cron%2Fmorsl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featurecreep-cron%2Fmorsl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featurecreep-cron%2Fmorsl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/featurecreep-cron","download_url":"https://codeload.github.com/featurecreep-cron/morsl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/featurecreep-cron%2Fmorsl/sbom","scorecard":{"id":1244532,"data":{"date":"2026-03-07T22:01:15Z","repo":{"name":"github.com/featurecreep-cron/morsl","commit":"2ccab607bf4e9c3a518e0f0c856d340ff03d3a17"},"scorecard":{"version":"v5.3.0","commit":"c22063e786c11f9dd714d777a687ff7c4599b600"},"score":5.1,"checks":[{"name":"CI-Tests","score":-1,"reason":"no pull request found","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#ci-tests"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#security-policy"}},{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yml:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dependency-update-tool"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'contents' permission set to 'read': .github/workflows/push.yml:21","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/release.yml:84","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yml:14","Warn: topLevel 'security-events' permission set to 'write': .github/workflows/codeql.yml:12","Warn: topLevel 'contents' permission set to 'write': .github/workflows/dependabot-automerge.yml:8","Warn: topLevel 'security-events' permission set to 'write': .github/workflows/push.yml:10","Warn: topLevel 'contents' permission set to 'write': .github/workflows/push.yml:8","Warn: topLevel 'packages' permission set to 'write': .github/workflows/push.yml:9","Warn: topLevel 'security-events' permission set to 'write': .github/workflows/release.yml:10","Warn: topLevel 'contents' permission set to 'write': .github/workflows/release.yml:8","Warn: topLevel 'packages' permission set to 'write': .github/workflows/release.yml:9","Info: topLevel permissions set to 'read-all': .github/workflows/scorecard.yml:10"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#binary-artifacts"}},{"name":"SAST","score":10,"reason":"SAST tool detected: CodeQL","details":["Info: SAST configuration detected: CodeQL","Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#vulnerabilities"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:58: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:67: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/ci.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/codeql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/codeql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/codeql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/codeql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:107: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:110: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:119: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:126: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:134: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:44: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:68: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:71: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:74: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:77: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:85: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/push.yml:93: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/push.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:27: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:47: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:61: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:68: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:76: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:86: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:89: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/scorecard.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/scorecard.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/scorecard.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/scorecard.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/scorecard.yml:33: update your workflow using https://app.stepsecurity.io/secureworkflow/featurecreep-cron/morsl/scorecard.yml/main?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:1: pin your Docker image by updating python:3.12-slim to python:3.12-slim@sha256:ccc7089399c8bb65dd1fb3ed6d55efa538a3f5e7fca3f5988ac3b5b87e593bf0","Warn: pipCommand not pinned by hash: Dockerfile:19-23","Warn: downloadThenRun not pinned by hash: morsl.sh:73","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:29","Warn: pipCommand not pinned by hash: .github/workflows/ci.yml:53","Info:   0 out of  23 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of  16 third-party GitHubAction dependencies pinned","Info:   0 out of   1 containerImage dependencies pinned","Info:   0 out of   3 pipCommand dependencies pinned","Info:   0 out of   1 downloadThenRun dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#pinned-dependencies"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#packaging"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#branch-protection"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#fuzzing"}},{"name":"Contributors","score":3,"reason":"project has 1 contributing companies or organizations -- score normalized to 3","details":["Info: found contributions from: featurecreep-dev"],"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#contributors"}}]},"last_synced_at":"2026-03-08T06:03:24.157Z","repository_id":342891247,"created_at":"2026-03-08T06:03:24.157Z","updated_at":"2026-03-08T06:03:24.157Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291119,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","homelab","meal-planner","menu-generator","recipe","self-hosted","tandoor"],"created_at":"2026-03-07T23:03:31.718Z","updated_at":"2026-04-01T19:19:26.895Z","avatar_url":"https://github.com/featurecreep-cron.png","language":"Python","funding_links":["https://buymeacoffee.com/featurecreep"],"categories":[],"sub_categories":[],"readme":"# Morsl\n\n**Generate weekly menus from your Tandoor Recipes collection.**\n\nMorsl connects to your [Tandoor Recipes](https://github.com/TandoorRecipes/recipes) instance, picks recipes based on your preferences (keywords, ratings, ingredients, cook history), and serves a menu your household can browse and order from. Selections sync back to Tandoor as meal plans.\n\n[![CI](https://github.com/featurecreep-cron/morsl/actions/workflows/ci.yml/badge.svg)](https://github.com/featurecreep-cron/morsl/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/featurecreep-cron/morsl/graph/badge.svg)](https://codecov.io/gh/featurecreep-cron/morsl)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/featurecreep-cron/morsl/badge)](https://scorecard.dev/viewer/?uri=github.com/featurecreep-cron/morsl)\n[![License: MIT](https://img.shields.io/github/license/featurecreep-cron/morsl)](LICENSE)\n[![Release](https://img.shields.io/github/v/release/featurecreep-cron/morsl)](https://github.com/featurecreep-cron/morsl/releases)\n[![Python](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Ffeaturecreep-cron%2Fmorsl%2Fmain%2Fpyproject.toml)](https://www.python.org/downloads/)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![GHCR](https://img.shields.io/badge/ghcr.io-morsl-blue?logo=docker)](https://github.com/featurecreep-cron/morsl/pkgs/container/morsl)\n\n---\n\n![Customer menu view — recipe cards with photos, ratings, ingredients, and ordering](docs/screenshot-customer-menu.png)\n\n---\n\n## What You Need\n\n- A running [Tandoor Recipes](https://docs.tandoor.dev) instance\n- A Tandoor API token (in Tandoor: **Settings \u003e API Tokens \u003e Create** — just click Create and copy the token string)\n- Docker installed on your server\n\n## Quick Start\n\n### Docker Compose (recommended)\n\nCreate a `docker-compose.yml`:\n\n```yaml\nservices:\n  morsl:\n    image: ghcr.io/featurecreep-cron/morsl:latest\n    ports:\n      - \"8321:8321\"\n    environment:\n      - TANDOOR_URL=http://your-tandoor-address:port  # e.g. http://192.168.1.50:8080\n      - TANDOOR_TOKEN=your-api-token\n      - TZ=America/New_York\n      - PUID=1000  # match your host user: id -u\n      - PGID=1000  # match your host group: id -g\n    volumes:\n      - morsl-data:/app/data\n    restart: unless-stopped\n\nvolumes:\n  morsl-data:\n```\n\n```bash\ndocker compose up -d\n```\n\nOpen `http://your-server:8321`. The setup wizard walks you through connecting to Tandoor if you skip the environment variables.\n\n![Setup wizard — guided configuration with profile presets](docs/screenshot-setup.png)\n\nThe `data` volume keeps your profiles, schedules, branding, and settings safe across updates.\n\n### Docker run (quick test)\n\n```bash\ndocker run -d --name morsl \\\n  -e TANDOOR_URL=http://your-tandoor-address:port \\\n  -e TANDOOR_TOKEN=your-api-token \\\n  -p 8321:8321 \\\n  ghcr.io/featurecreep-cron/morsl:latest\n```\n\n**Note:** Without a volume mount (`-v`), your settings are lost when the container restarts. Use Docker Compose for anything permanent.\n\n### Updating\n\n```bash\ndocker compose pull \u0026\u0026 docker compose up -d\n```\n\nYour data (profiles, schedules, branding, settings) is stored in the volume and survives updates.\n\n---\n\n## How It Works\n\n1. **Create profiles** in the admin panel (`/admin`). Each profile defines what kind of recipes to pick — \"Weeknight Dinner\" might require the keyword \"entree\" and prefer 4+ star recipes. \"Breakfast\" might pick 3 recipes tagged \"breakfast.\"\n2. **Generate a menu.** Morsl picks recipes that match your rules. If your rules are too strict, it picks fewer recipes rather than giving you nothing.\n3. **Share the menu.** Your household opens `http://your-server:8321` on their phone or computer — no accounts needed. They browse recipe cards, see photos and ingredients, and tap to order.\n4. **Orders appear** instantly in the admin panel. You can sync selections back to Tandoor as meal plan entries with one click.\n\n---\n\n## Features\n\n- **Multiple profiles** — \"Weeknight Dinner,\" \"Breakfast,\" \"Weekend Projects\" — each with its own rules\n\n![Profile editor — keyword filters, requirement levels, flexible rules](docs/screenshot-profile-editor.png)\n\n- **Smart filtering** — filter by keywords, minimum rating, ingredients, date ranges, recipe books. Rules can be strict (must match) or flexible (prefer but don't require)\n- **Household menu** — shareable page where your family browses and orders, no accounts needed\n- **Live order notifications** — the admin panel updates instantly when someone places an order\n- **Tandoor meal plan sync** — push selections back to Tandoor\n- **Weekly plans** — plan your whole week: assign different profiles to different days and meals. Monday breakfast from \"Quick Breakfast,\" Monday dinner from \"Weeknight Dinner,\" Saturday dinner from \"Weekend Projects\" — all generated at once\n\n![Weekly planner — assign profiles to days and meal types](docs/screenshot-weekly-planner.png)\n- **Scheduled generation** — automatic menu refresh on a schedule\n- **Custom branding** — your own logo, favicon, app name, and slogans\n- **Mobile-friendly** — responsive layout, QR codes for easy sharing from desktop\n- **Setup wizard** — guided 6-step configuration, no config files required\n- **Works with any recipe collection** — even if your recipes aren't tagged or rated, Morsl still picks from them. Tags and ratings just give you more control.\n\n![Mobile view — designed for phones, share the menu link with your household](docs/screenshot-mobile.png)\n\n---\n\n## Configuration\n\n### Environment variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `TANDOOR_URL` | *(none)* | Your Tandoor instance URL |\n| `TANDOOR_TOKEN` | *(none)* | Tandoor API token |\n| `TZ` | `UTC` | Timezone for schedules and meal plans |\n| `LOG_LEVEL` | `INFO` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |\n| `LOG_TO_STDOUT` | `1` (Docker) | Send logs to stdout instead of file |\n| `PUID` | `1000` | User ID for file ownership (see below) |\n| `PGID` | `1000` | Group ID for file ownership (see below) |\n\nBoth `TANDOOR_URL` and `TANDOOR_TOKEN` can also be configured through the setup wizard.\n\nThe API token needs read access to recipes, keywords, and books. If you want to sync orders back to Tandoor as meal plans, it also needs write access to meal plans. Tandoor tokens have full access by default, so a freshly created token works.\n\n### Admin panel\n\nEverything is configured through the admin UI at `/admin`:\n\n![Admin panel — generate menus, manage profiles, configure schedules](docs/screenshot-admin.png)\n\n- **Generate** — pick a profile, generate a menu, set up automatic schedules\n- **Profiles** — create and edit profiles with filtering rules\n- **Weekly** — build multi-day menu templates (different profiles per day and meal type)\n- **Settings** — branding, Tandoor connection, data management\n\nThree complexity tiers (Standard / Advanced / Expert) progressively show more options. Start with Standard — it covers most use cases.\n\nAPI documentation is available at `/docs` (interactive) and `/redoc` (reference).\n\n### Security\n\nMorsl has no built-in authentication by default. If you're only using it at home on your local network, this is fine. The optional admin PIN (Settings) keeps household members out of the admin panel — it is **not** a substitute for real authentication.\n\nIf you're exposing Morsl to the internet, put it behind a reverse proxy with proper authentication (Authelia, Authentik, Cloudflare Access, etc.).\n\n**Forgot your PIN?** Two options:\n\n1. **Script** — run inside the container:\n   ```bash\n   docker exec morsl python scripts/reset-pin.py\n   ```\n\n2. **Manual** — edit `settings.json` in your data volume directly:\n   ```json\n   {\n     \"pin\": \"\",\n     \"admin_pin_enabled\": false,\n     \"kiosk_pin_enabled\": false\n   }\n   ```\n\nEither way, restart the container afterward, then set a new PIN from admin settings.\n\n---\n\n## Backup and Restore\n\nAll Morsl data lives in the `data` volume. To back up:\n\n```bash\ndocker compose stop morsl\ndocker run --rm -v morsl-data:/data -v $(pwd):/backup alpine tar czf /backup/morsl-backup.tar.gz -C /data .\ndocker compose start morsl\n```\n\nTo restore on a new server:\n\n```bash\ndocker run --rm -v morsl-data:/data -v $(pwd):/backup alpine tar xzf /backup/morsl-backup.tar.gz -C /data\ndocker compose up -d\n```\n\n**What's in the volume:**\n\n| File/Directory | Contents |\n|---|---|\n| `settings.json` | App settings, Tandoor credentials (base64-encoded token), PIN |\n| `profiles/` | Recipe filtering profiles (one JSON per profile) |\n| `templates/` | Weekly meal plan templates |\n| `weekly_plans/` | Generated weekly plans |\n| `history.json` | Generation history |\n| `schedules.json` | Automatic generation schedules |\n| `branding/` | Uploaded logo, favicon, loading icon |\n\n---\n\n## Development\n\n\u003cdetails\u003e\n\u003csummary\u003eFor contributors and developers\u003c/summary\u003e\n\nRequires Python 3.12+ and system libraries for CairoSVG (`libcairo2`, `libpango-1.0-0`).\n\n```bash\npip install -e \".[dev]\"\npytest\n```\n\n200+ tests covering the solver, services, API routes, models, and utilities. Integration tests requiring a live Tandoor instance are marked `@pytest.mark.integration` and skipped by default.\n\n### Tech stack\n\n- **Backend**: FastAPI, Pydantic, uvicorn\n- **Solver**: PuLP with CBC (COIN-OR) — linear programming for recipe selection\n- **Frontend**: Vanilla JS with Alpine.js, no build step\n- **Scheduling**: APScheduler 3.x\n- **Container**: python:3.12-slim (Debian), multi-arch (amd64 + arm64), auto-release pipeline\n\nThe Docker image uses `python:3.12-slim` rather than Alpine because CairoSVG and Pango require system libraries that are simpler to install on Debian.\n\nThe image runs as UID 1000 by default. Set `PUID` and `PGID` environment variables to match your host user — no rebuild needed. This follows the [linuxserver.io convention](https://docs.linuxserver.io/general/understanding-puid-and-pgid/).\n\n### Project structure\n\n```\nmorsl/                         # Python package\n├── __init__.py\n├── solver.py                  # PuLP-based recipe picker\n├── models.py                  # Domain models\n├── tandoor_api.py             # Tandoor API client\n├── constants.py               # Configuration constants\n├── utils.py                   # Shared utilities\n├── app/                       # FastAPI application\n│   ├── main.py                # Lifespan, middleware, page routes\n│   ├── config.py              # Pydantic Settings\n│   └── api/\n│       ├── dependencies.py    # DI singletons, auth\n│       ├── models.py          # Request/response schemas\n│       └── routes/            # API endpoints\n└── services/                  # Business logic layer\nweb/                           # Frontend (vanilla JS, Alpine.js)\ntests/                         # 200+ tests\nDockerfile\ndocker-compose.yml\n```\n\n\u003c/details\u003e\n\n---\n\n## Support\n\n- [File an issue](https://github.com/featurecreep-cron/morsl/issues) on GitHub\n- [Buy me a coffee](https://buymeacoffee.com/featurecreep)\n- Built by [Cron](https://featurecreep.dev)\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffeaturecreep-cron%2Fmorsl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffeaturecreep-cron%2Fmorsl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffeaturecreep-cron%2Fmorsl/lists"}