Tandoor Recipes
tandoorrecipes/recipes · Self-hosted recipe manager · Django + Vue 3
Tandoor is a mature, well-loved self-hosted application
— 8,756 commits over eight years, 532 contributors, a real
Django + Vue 3 stack with a real user base. The grade is F
because the same risks recur in layers: secrets are committed in
the Kubernetes install docs, the Docker and K8s manifests run as
root without resource limits, the application code swallows 110
exceptions across the Python backend, and the complexity check
— now run in-process across every language — scores
0. Test coverage is 9% of source files. The structural debt is
concentrated in three files everyone will recognise:
cookbook/models.py, cookbook/views/api.py,
and the Vue OpenAPI client.
What changed since our last scan
Our first analysis scored Tandoor 44. The latest inkode scores it 31. The codebase barely moved — the drop is almost entirely the scanner getting more thorough:
- Complexity went from 50 to 0. Last time the complexity category was a near-pass, measured for Python only. inkode now analyses cyclomatic complexity, nesting depth, function length and parameter count in-process for Python, TypeScript, Vue and JavaScript. The result: 97 functions over the threshold, 149 deep-nesting sites, 34 over-parameterised functions. Much of this is the vendored PDF.js bundle (one function clocks complexity 2,699), but the application code carries its share too.
- Magic-number detection got precise. Now driven by pylint and eslint, the count dropped from 495 to 84 — the old number was mostly Python false positives.
- Git history is read in full. We now see the real shape of the repo: 8,756 commits across eight years and 532 contributors, where the earlier pass undercounted.
The security and structural findings — 15 secrets, 21 infra issues, the 244-degree god package — are essentially unchanged. What's new is that inkode now sees the complexity that was always there.
Category Scores
Stack profile
A clean Django backend + Vue 3 frontend split. HTML test fixtures (47.6% of bytes) and the JSON localisation bundles dominate the byte mix; Python and TypeScript carry the application logic.
Key Findings
Secret Scanning — in the install docs and test fixtures
Detects hardcoded API keys, tokens, and credentials
Fifteen hardcoded secrets — 5 in the Kubernetes
install documentation and 10 in the scraped-HTML test
fixtures. The single worst offender is
docs/install/k8s/15-secrets.yaml: four generic
API keys, plus the same file flagged again by the
kubernetes-secret-yaml rule. The remaining 10
are API keys embedded in scraped HTML pages from recipe
sites — tasteofhome.html (4),
foodnetwork.html (2), and one each from
delish, thespruceeats, journaldesfemmes, giallozafferano.
Those deserve an allowlist, not a rotation; the K8s ones
deserve a rotation and a placeholder commit.
Why this is the highest-severity finding.
A committed secret is a one-way door. Once it's in the git
history it's archived by GitHub, by scrapers, and by anyone
who cloned the repo — deleting it from
HEAD doesn't unpublish it. The K8s manifest is
flagged twice because the scanner recognises both the literal
key pattern and the Kubernetes Secret kind that
wraps it. The HTML-fixture secrets are a different story:
they're keys that already leaked from public recipe websites
years ago, captured here as test data. Both still trigger
the scanner; only the K8s ones are an actionable security
risk. (inkode redacts the matched value before anything
leaves the machine, so the secret itself is never uploaded.)
docs/install/k8s/15-secrets.yaml (5 findings)Error Handling — 110 bare excepts on the Python side
Detects bare except:, empty catches, and other error-swallowing patterns
110 bare except: clauses in the Django backend.
Bare excepts swallow KeyboardInterrupt and
SystemExit alongside real bugs; in a long-running
web service, they're the most common path from
this-should-have-thrown to silent data
corruption. Pin each to a specific exception type or, at
minimum, except Exception.
What this looks like in production. A user
imports a recipe from a third-party site. The HTML parser hits
an edge case — a Unicode quirk, a malformed timestamp,
anything. The bare except: catches it, the import
succeeds with empty fields, and the user gets an
almost-right recipe with no error message. Multiply by a
thousand users and the bug-report channel fills with
“sometimes my import is missing fields.”
Nobody can reproduce it because the original exception was
thrown away three layers down the stack. 110 of these is a
lot of places this can happen.
Complexity — 97 over-threshold functions
Cyclomatic complexity, nesting depth, function length, parameter count
97 functions exceed the cyclomatic-complexity threshold of 10
(71 Python, 24 JavaScript, 2 TypeScript), plus 149 deep-nesting
sites and 34 over-parameterised functions. The extreme outliers
are vendored: the bundled PDF.js worker contains a function at
complexity 2,699. The application code that matters is
cookbook/integration/mealie1.py's
get_recipe_from_file at complexity 84 — an
import path, which is exactly where untested complexity is most
dangerous.
Why this category scores 0. Complexity is now
measured in-process across every language, not Python-only. The
vendored PDF.js bundle inflates the raw count, but stripping it
out still leaves dozens of genuinely branchy functions in the
recipe-import integrations — the same code that has no
tests (below) and swallows exceptions (above). Add
cookbook/static/pdfjs/** to .ik.yaml to
see the real application-code complexity clearly.
get_recipe_from_file — CC 84Infrastructure — root user, no probes, unpinned images
Audits Dockerfiles, Kubernetes manifests, and Terraform
One Dockerfile and nine Kubernetes manifests scanned, 21 issues.
The Dockerfile runs as root and ships without a
HEALTHCHECK. K8s manifests in the install docs are
missing resource limits, liveness/readiness probes, and
runAsNonRoot; one references
nginx:latest instead of a pinned digest. Both the
Postgres StatefulSet and the application Deployment use single
replicas — fine for self-host docs, but worth flagging.
Why the install docs are the right place to fix this.
Self-host install docs are the operational template for
thousands of deployments. The user copy-pastes the YAML once
and never thinks about it again. Every weak default in those
files becomes a weak default on every Tandoor install in the
wild. runAsNonRoot: false means a container
breakout reaches the node as root. nginx:latest
means the same install behaves differently depending on which
day you ran it. Missing probes mean a wedged container keeps
receiving traffic. These are template hygiene problems with
large reach.
Import Graph — the three load-bearing modules
Analyses package dependencies for coupling, cycles, and god packages
Three modules concentrate most of the coupling.
vue3/src/openapi/models/index.ts is a barrel
re-exporting 241 OpenAPI model files
(combined degree 244). vue3/src/openapi/runtime.ts
is imported by 240 modules — the universal client base
class. On the backend, cookbook/models.py is
imported by 127 other packages, with one self-import giving it
degree 128. Touch any of these and the change ripples.
No circular dependencies.
The one that costs you sleep is models.py.
The Vue barrel is generated; treat it like the schema file it
is. The runtime base class is fine as a single source of
truth. cookbook/models.py, however, is the
Django ORM definitions for the whole app — 127
importers means every domain (recipes, meal plans, shopping
lists, users) reaches into one file. Any change to a model
field forces 127 imports to recompile and risks breaking
unrelated areas. The classic Django split here is per-domain
models/ packages with a shared base; the
refactor itself is mechanical, but the planning matters
because the imports are everywhere.
vue3/src/openapi/models/index.ts — degree 244Test Presence — 9% ratio, 24 untested directories
Measures test-to-source file ratio per directory
60 test files for 645 source files — a 9% ratio.
24 directories with source code have no tests next to them.
The entire Vue 3 frontend (stores, composables, utils, 240+
OpenAPI models) is untested. On the backend,
cookbook/helper/, cookbook/integration/,
and cookbook/views/ are largely
untested. pytest is configured; the gap is content, not setup.
Why 9% is the score that should worry the maintainers most.
Tandoor has the structural debt (god packages, hotspots,
oversized files, complexity) and doesn't have the safety
net to refactor it out. Every cleanup change becomes a coin flip:
does anything still work afterwards? The 24 untested
directories include the recipe-import integrations —
the feature most likely to break silently because every
third-party site changes its HTML over time, and the same code
that tops the complexity list. Starting tests in
cookbook/integration/ would pay back the fastest.
Change Risk — hotspots and coupling concentrate in api.py
Files ranked by change frequency, plus pairs that co-change in git history
Across 1,433 commits in the last year and 735 changed files, the
top hotspots are cookbook/views/api.py, the German
Vue locale bundle, and cookbook/serializer.py. The
coupling check flags 674 file pairs — mostly the locale
bundles moving together. The cross-cutting pair to watch is
api.py ↔ serializer.py: the same two
files keep changing in lockstep, which is the signature of a
hidden contract worth surfacing.
Why api.py + serializer.py is
the diagnostic. A view file and a serializer file
moving together commit after commit means the
request/response shape lives in two places that nobody has
unified. When somebody adds a new field they have to edit
both, and when they forget one, you ship an endpoint that
returns 200 with a missing key. That's the “sometimes
the field is null” bug-class. The mechanical fix is to
co-locate the serializer with its view (or merge them); the
deeper one is to define the contract once and let both sides
consume it.
Code health — magic numbers and oversized files
Inline literals and files above the line-count threshold
84 magic-number literals (83 JavaScript, 1 Python) — down
from 495 now that pylint and eslint drive the detection instead
of a regex heuristic. 144 files exceed the 500-line warning
threshold, 28 exceed 1,000 lines. The worst offenders are
vendored or generated: pdf.worker.mjs (57k lines),
ApiApi.ts, openapi.json. Among
application code, cookbook/views/api.py and
cookbook/serializer.py are the natural split
candidates — they're also the two files at the top of
the hotspot list.
Reading the pattern across these checks. The
same handful of files keep showing up: api.py,
serializer.py, models.py. They're
large, they're imported everywhere, they change often, and
they have hidden contracts with each other. None of the
individual signals is severe enough to act on alone. The
cross-check coincidence is what makes it actionable: this is
where you spend your refactor budget.
Checks that passed
vulture (Python) and knip (JS/TS) found no unused symbols.
No known CVEs across 66 Python dependencies and the npm tree.
No token-level duplicate blocks; 2 semantic near-duplicate pairs surfaced for review.
Zero circular dependencies across 714 packages and 1,144 edges.
134 TODOs across 581 files — well below the density threshold.
Map of the top connectors
These seven files concentrate most of the structural weight by
import degree. The generated Vue OpenAPI client tops the list;
cookbook/models.py is the load-bearing backend module.
cookbook/views/api.py sits just below by degree but
tops the hotspot list — the canonical “changes often,
imported widely” file.
Top Risk Files
Takeaways
- Rotate the four hardcoded keys in
docs/install/k8s/15-secrets.yamland replace with placeholder values + a one-line note. - Allowlist
cookbook/tests/other/test_data/*.htmlin the secrets config — those are scraped fixtures, not credentials. - Add a non-root
USERandHEALTHCHECKto the Dockerfile; set resource limits, probes, andsecurityContext: runAsNonRooton every K8s manifest in the install docs. - Pin the
nginx:latestreference to a specific tag or digest. - Triage the 110 bare
except:clauses by file — start withcookbook/views/andcookbook/integration/where they sit on user-facing code paths.
- Add tests for the 24 untested directories, prioritising
cookbook/integration/andcookbook/helper/— the same code that tops the complexity list. - Split
cookbook/views/api.pyandcookbook/models.pyinto domain modules; both sit at the top of the hotspot and import-graph lists. - Surface the
api.py↔serializer.pyco-change pattern in code: either co-locate the serialiser with its view or document the contract. - Exclude vendored / generated files (pdfjs, openapi) from line-count and complexity checks via
.ik.yaml— collapses most of the structural noise and reveals the real application-code complexity.
See how your codebase scores
Run inkode against your repo in under a minute. No account required.
Scan Your Repo