Files
Epicure/FEATURE_AUDIT.md
Arnaud f0632cce95 feat: USDA FoodData Central per-ingredient nutrition lookup (v0.70.0)
estimateNutrition() (lib/ai/features/estimate-nutrition.ts) is now a
hybrid: for each ingredient, estimateGrams() (lib/ingredient-grams.ts)
converts its quantity+unit to grams where possible (weight units
exactly; volume units -- cup/tbsp/tsp/ml/l/etc -- via a 1ml~=1g water-
density approximation, documented as a real simplification but far
better than skipping them). Convertible ingredients get looked up in
USDA FoodData Central (lib/usda.ts, SR Legacy + Survey (FNDDS)
datasets -- the two suited to generic/raw ingredients, not Branded
packaged products or narrower Foundation) and summed. Whatever's left
(count-based units like "2 cloves", no USDA match, or USDA_API_KEY
unset entirely) goes through one AI call asking for just that
subset's total contribution, which sums directly with the USDA
totals -- no whole-recipe AI estimate to reconcile against.

Same exported signature and return shape as before
(NutritionEstimate / {perServing: {...}}), so every existing caller
(the nutrition route, meal-plan generation) gets more accurate
results automatically once USDA_API_KEY is set in Admin -> Settings,
with zero behavior change when it isn't.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 00:41:12 +02:00

19 KiB
Raw Permalink Blame History

Epicure Feature Audit

Code-verified inventory of what exists in the app today, by domain. Written 2026-07-21 after a full read of the routes/schema/components listed under "key files" for each entry — not guessed from names. Keep this file updated whenever a feature is added or changes what it does (see standing rule below).

Status legend: Exists (fully working) · Partial (works but with a real limitation, noted) · Missing (confirmed absent by grep/read, not just "didn't find it").


1. Recipes & AI

AI stack: Vercel AI SDK, provider-agnostic (apps/web/lib/ai/factory.ts) — OpenAI, Anthropic, OpenRouter, Ollama (local). Key resolution order: user's own BYOK key → admin default per use-case → admin's raw provider key → env var. Structured outputs always generateObject + Zod; chat features use generateText with optional tool-calling.

Feature Status Description Key files
Recipe CRUD Exists Create/get/list/update/delete; update snapshots the prior version first (recipeSnapshots) apps/web/app/api/v1/recipes/**
Fork / duplicate Exists Same backend action for both — UI just swaps the label based on ownership apps/web/app/api/v1/recipes/[id]/fork/route.ts
Version history Exists GET /recipes/[id]/versions, diff-able snapshots apps/web/app/api/v1/recipes/[id]/versions/**
Import from URL Exists Fetches a page (SSRF-checked), strips markup, extracts a structured recipe via AI. Returns the extraction to the client — doesn't self-persist, caller POSTs it to create apps/web/app/api/v1/ai/import-url
Import from photo Exists Two-stage vision→text; recognizes a photographed page/handwritten recipe, self-persists as a private recipe apps/web/app/api/v1/ai/import-photo, lib/ai/features/{recognize-photo,generate-recipe-from-recognition}.ts
Generate from idea/title Exists Text-only generation from just a title apps/web/app/api/v1/ai/generate-from-idea
Recipe ideas (browsing) Exists 6 idea cards from an optional prompt, not persisted apps/web/app/api/v1/ai/recipe-ideas
Adapt (exclude ingredients / constraints) Exists Persists as new recipe + variation link, unless AI reports unchanged apps/web/app/api/v1/ai/adapt/[id]
Suggest variations Exists Ideas only, not persisted; gated by tier feature flag recipe_variations apps/web/app/api/v1/ai/variations/[id]
Meal / drink pairings Exists Gated by tier feature flags meal_pairing / drink_pairing apps/web/app/api/v1/ai/{pairings,drinks}/[id]
Translate recipe Exists Saves as a new private recipe, keeps original quantities/units/timers apps/web/app/api/v1/ai/translate/[id]
Batch-cook generator Exists One recipe, multiple dishes, per-dish fridge/freezer guidance apps/web/app/api/v1/ai/batch-cook/generate
Nutrition estimation Exists AI-estimated, cached on the recipe row; author-triggered apps/web/app/api/v1/recipes/[id]/nutrition
Scale servings Exists Model-driven, not naive linear math (handles "1 egg" etc.) apps/web/app/api/v1/ai/scale
Substitute ingredient Exists apps/web/app/api/v1/ai/substitute
Recipe chat / cooking chat Exists Recipe-grounded Q&A, and a general assistant with tool-calling (createRecipe, addToShoppingList) apps/web/app/api/v1/ai/{recipe-chat,cooking-chat}
Themed meal generator Exists Multi-course meal into a collection apps/web/app/api/v1/ai/generate-meal
Recipe media: photos Exists S3-compatible storage, counts against tier storage limit packages/db/src/schema/recipes.ts (recipePhotos)
Recipe video Missing No video field/upload path anywhere in schema or components
Collections Exists Shared/collaborative (collection_members, viewer/editor roles), fork-a-collection, explore page, reorder apps/web/app/api/v1/collections/**
Tags, difficulty, dietary labels Exists Dietary schema has 7 tags; search only exposes 4 of them as filters (vegan/vegetarian/glutenFree/dairyFree) packages/db/src/schema/recipes.ts
Visibility (private/unlisted/public/followers) Exists, one flagged gap Enforced almost everywhere as public-vs-not-private; the /r/[id] anonymous share route's own code comment flags that followers-only recipes may be reachable without a follow-relationship check apps/web/app/r/[id]/page.tsx
Print views Exists Recipe, collection, meal plan, shopping list, pantry all have print routes apps/web/app/print/**
Markdown export Exists Single recipe, bulk (up to 100), meal plan apps/web/lib/markdown/**
Cooking mode Exists Step timers (multiple parallel), voice control (Web Speech API, EN/FR commands), wake-lock, batch-cook step grouping apps/web/components/cooking-mode/cooking-mode.tsx
Search Exists Title/description/ingredient/tag substring match + filters (difficulty, type, max time, dietary, exact tags) apps/web/app/api/v1/search
"What can I cook" (pantry-matched) Exists Deterministic ingredient-overlap scorer against the user's own recipes, not AI apps/web/lib/pantry-match.ts, /recipes/can-cook
Explore / trending Exists Purely favorite-count-driven (7-day window) + recency — no editorial curation apps/web/app/(app)/explore

2. Meal planning, pantry, shopping, nutrition

Feature Status Description Key files
Weekly calendar meal plan Exists MonSun grid, manual CRUD + bulk-set-week apps/web/app/(app)/meal-plan, apps/web/app/api/v1/meal-plans/[weekStart]/**
AI weekly meal-plan generator Exists Pantry-aware, dietary-aware, nudged by nutrition goals; one real recipe per entry apps/web/app/api/v1/ai/meal-plan/generate
Meal plan sharing Exists meal_plan_members viewer/editor roles for collaborators, plus an anonymous read-only public link (isPublic flag, mirrors shopping lists' but view-only — no public-editable mode) at /m/[id], with a QR code generated client-side for the link apps/web/lib/meal-plan-access.ts, apps/web/app/m/[id]/page.tsx, apps/web/components/meal-plan/share-meal-plan-button.tsx
Calendar export (.ics) Exists Real ICS builder with per-meal-type wall-clock times apps/web/app/api/v1/meal-plans/[weekStart]/export/ics
Weekly nutrition rollup Exists apps/web/app/api/v1/meal-plans/[weekStart]/nutrition
Multiple shopping lists Exists apps/web/app/api/v1/shopping-lists/**
Generate list from meal-plan week Exists Merges duplicate ingredients, flags items already covered by pantry (never silently drops) apps/web/lib/pantry-shopping-match.ts
Shared/collaborative lists Exists Member roles and an anonymous public link mode (isPublic/publicEditable) packages/db/src/schema/meal-planning.ts
List collaboration Partial Short-polling, not websocket/push — explicitly documented as such in code apps/web/components/meal-plan/shopping-list-view.tsx
Aisle categorization Exists Heuristic auto-assign + bulk re-categorize apps/web/lib/grocery-categories.ts
Grocery delivery integration Partial / stub Generic export payload works; Instacart adapter is an explicit stub (requires a partnership agreement Epicure doesn't have — returns 501 if unconfigured, throws if "configured") apps/web/lib/grocery-providers/instacart.ts
Other delivery/price integrations (DoorDash, Kroger, Walmart, live pricing) Missing Confirmed absent by repo-wide search
Pantry manual CRUD Exists apps/web/app/api/v1/pantry/**
Auto-deduct pantry on cook Exists, one nuance Name+unit matching; for batch-cook recipes, deduction fires once (not re-scaled) on the first dish marked cooked apps/web/app/api/v1/recipes/[id]/cooked/route.ts
Expiring-soon tracking Exists With "use it up" recipe suggestions apps/web/components/pantry/expiring-soon-suggestions.tsx
Barcode scan (pantry) Exists Real Open Food Facts API lookup by UPC/EAN apps/web/app/api/v1/pantry/scan/barcode
Photo scan (pantry) Exists Vision-model based apps/web/app/api/v1/pantry/scan/photo
Nutrition goals Exists apps/web/app/api/v1/users/me/nutrition-goals
Nutrition diary Exists Single-day diary view, plus a Trend tab (7/30/90-day calorie line chart + daily macro averages) — the same endpoint switches modes via a range query param apps/web/app/api/v1/users/me/nutrition-diary, apps/web/components/nutrition/nutrition-trend.tsx
USDA nutrition-facts database Exists estimateNutrition now looks up each ingredient in USDA FoodData Central (SR Legacy + Survey (FNDDS) datasets) after converting its quantity/unit to grams, sums the matches, and only falls back to AI estimation for ingredients that couldn't be converted (count-based units like "2 cloves") or had no USDA match — including every ingredient, transparently, when USDA_API_KEY isn't configured. Barcode scan (pantry) still only IDs product names via Open Food Facts, unrelated to this. apps/web/lib/usda.ts, apps/web/lib/ingredient-grams.ts, apps/web/lib/ai/features/estimate-nutrition.ts
Batch cooking Exists Shared prep steps grouped by which dish they serve, per-dish fridge/freezer countdown independent of the whole-batch pantry deduction apps/web/components/recipe/batch-cook-*.tsx

3. Social, messaging, notifications

Feature Status Description Key files
Follow / unfollow Exists One-way, no approval step apps/web/app/api/v1/users/[username]/follow
Block Exists Hides comments, blocks messaging both ways apps/web/lib/blocks.ts
Private accounts Exists Hides recipe grid from non-followers; excluded from "for you" unless already followed apps/web/app/(app)/u/[username]/page.tsx
Profile page Exists Cooking-history streak stats, "cooked it" photo gallery (owner-visible) apps/web/app/(app)/u/[username]
Ratings/reviews, comments (1-level threaded), reactions, @mentions Exists packages/db/src/schema/social.ts
Favorites Exists Drives "for you" ranking and trending apps/web/app/api/v1/recipes/[id]/favorite
Direct messaging Exists, 1:1 by design No group messaging (deliberate — unique-pair constraint at schema level) packages/db/src/schema/messaging.ts
Read receipts Partial Thread-level lastReadAt only — no per-message delivery/seen state packages/db/src/schema/messaging.ts
Following feed Exists apps/web/app/api/v1/feed
"For You" personalized feed Exists Tag/dietary preference profile from favorites + high ratings apps/web/lib/for-you-ranking.ts
Trending feed / explore Exists Favorite-count driven, no editorial curation, no separate "featured" surface apps/web/app/api/v1/feed/trending
Notification categories Exists 8 categories, independent push + email toggle each, plus email-only weekly digest packages/db/src/schema/users.ts (userNotificationPrefs)
Push notifications Exists Web Push + VAPID, end-to-end, real trigger call sites (not just plumbing) apps/web/lib/push.ts
Push display + click-through Exists sw.js previously had no push listener at all — push messages never displayed. Now has both push (calls showNotification) and notificationclick (focuses an existing tab on the payload's url, or opens one) apps/web/public/sw.js
Email notifications + weekly digest cron Exists apps/web/lib/notifications.ts, apps/web/app/api/internal/cron/weekly-digest
Reports/moderation Exists Report → admin webhook → admin queue → resolve (reviewed/dismissed) + audit log apps/web/app/api/v1/reports, apps/web/app/api/v1/admin/reports
Moderator role Exists Moderators get /admin/reports (view/resolve) and /admin/recipes (view + unpublish takedown); every other /admin/* page redirects them out via requireFullAdminPage(). Comment deletion already allowed moderator before this. apps/web/lib/require-admin-page.ts, apps/web/app/admin/layout.tsx, apps/web/app/api/v1/admin/recipes/[id]/route.ts

4. Admin, billing, ops, integrations

Feature Status Description Key files
Tiers (free/pro/family) + limits Exists Recipe count, AI calls/month, storage, public-recipe count; TOCTOU-safe via advisory lock + transaction apps/web/lib/tiers.ts
Stripe webhook (checkout/cancel) Exists Signature-verified, replay-protected, event-deduped — production quality apps/web/app/api/webhooks/stripe/route.ts
Stripe checkout session creation Missing No code anywhere creates a Checkout Session or exposes an "Upgrade" flow — the webhook consumer exists but nothing produces the event it consumes
Self-serve billing portal Missing No Stripe customer portal; tier changes today only happen via direct admin edit at /admin/users/[id]
Admin dashboard Exists 13 sections: overview, insights/analytics, users, invites, recipe moderation, reports, support, tiers, webhooks, audit logs, storage, AI config, site settings, changelog apps/web/app/admin/**
Feature flags (per-tier) vs feature prefs (per-user cosmetic) Exists, two distinct systems Flags gate 3 AI capabilities by tier; prefs let users hide 7 nav sections, no billing implication apps/web/lib/{feature-flags,feature-prefs}.ts
User webhooks (personal automation) Exists 7 events, Zapier-style apps/web/lib/webhooks.ts
Admin ops webhooks (site-wide) Exists 3 events (signup, ticket, report) apps/web/lib/admin-webhooks.ts
Support tickets ↔ Gitea two-way sync Exists, verified real Outbound (create/comment/close mirror to Gitea issue) + inbound (signed webhook receiver, dedup, loop-safe) apps/web/lib/gitea.ts, apps/web/app/api/webhooks/gitea/route.ts
Self-serve API keys Exists Up to 10 per user, scoped full/read, SHA-256 hashed at rest apps/web/app/api/v1/api-keys
Public OpenAPI spec + docs page Exists Scalar-rendered at /docs, spec at /api/v1/openapi.json apps/web/lib/openapi.ts
i18n Partial Only en and fr — no other locales apps/web/messages/*.json
2FA Exists TOTP + backup codes + OTP, passwordless-account-compatible Better Auth twoFactor plugin
Social login Exists Google always, GitHub/Discord/generic OIDC if configured apps/web/lib/auth/server.ts
Rate limiting Exists Two layers: Better Auth's Redis-backed built-in limiter (tighter on 2FA verify endpoints) + a general sliding-window limiter for AI/API routes apps/web/lib/rate-limit.ts
Invite-gated signup Exists Signups can be globally disabled, invite-only fallback; first-ever user auto-promoted to admin

5. PWA, offline, mobile

Feature Status Description Key files
Installability Exists Manifest + SVG icons (no raster/PNG fallback), Chromium beforeinstallprompt button, iOS Safari gets a static "tap Share" instructional banner (no programmatic install API exists on Safari — Apple restriction, not fixable) apps/web/app/manifest.ts, apps/web/components/pwa/install-prompt.tsx
Service worker Exists Cache-first for /cook pages, network-first-with-cache-populate elsewhere, /api/* bypassed apps/web/public/sw.js
Background Sync Exists Chromium via native Background Sync API; Safari/Firefox (no support at all) via an online-event fallback reading the same IndexedDB queue apps/web/lib/offline-queue.ts, apps/web/public/sw.js
Offline save-for-later Exists Per-recipe explicit pin + IndexedDB-tracked list shown on the /offline fallback page apps/web/lib/offline-db.ts, apps/web/components/recipe/save-offline-button.tsx
Offline scope Partial Recipe viewing + one write path (mark-cooked). Shopping lists and meal plan have no offline/queue coverage
Push (client + server) Exists end-to-end Real subscribe/unsubscribe with correct state sync, VAPID, multiple triggers wired apps/web/lib/push.ts
Voice control Exists Web Speech API in cooking mode (this was previously assumed missing — it isn't) apps/web/components/cooking-mode/cooking-mode.tsx
Barcode scanning Exists (pantry only) BarcodeDetector isn't used, but a manual-entry-triggered Open Food Facts API lookup covers the same use case apps/web/app/api/v1/pantry/scan/barcode
OS Share Target (share a URL into Epicure from another app) Exists (Chromium/Android only) Manifest share_target/recipes?title&text&url; extracts the shared link from either url or a URL substring in text (senders vary), auto-opens the existing import-from-URL dialog pre-filled and auto-imports. Safari has no Share Target API at all — same Apple-restriction story as install prompts. apps/web/app/manifest.ts, apps/web/app/(app)/recipes/page.tsx, apps/web/components/recipe/url-import-dialog.tsx
Home-screen widgets, Siri/Assistant shortcuts Missing, expected — N/A for a pure PWA without a native shell
Native mobile app (React Native/Capacitor/Expo) Missing Confirmed PWA-only, no native project anywhere in the monorepo

Real gaps (confirmed absent, worth considering)

Ranked roughly by likely value:

  1. Stripe checkout + billing portal — the webhook consumer is production-ready but nothing produces the checkout event or lets a user self-serve upgrade/cancel/view invoices. Biggest gap if monetizing seriously.
  2. Recipe video — no support at all.
  3. Nutrition trend/history view — closed 2026-07-22 (7/30/90-day calorie chart + macro averages).
  4. USDA/nutrition-database lookup — closed 2026-07-22 (per-ingredient, gram-convertible ingredients only; AI fills the rest).
  5. OS Share Target — closed 2026-07-21 (Chromium/Android only, no Safari equivalent exists).
  6. Push click-through handling — closed 2026-07-21 (push/notificationclick listeners added to sw.js).
  7. Anonymous public link for meal plans — closed 2026-07-21 (read-only, with a QR code).
  8. Grocery delivery/live pricing beyond the Instacart stub (which needs a partnership agreement to go live).
  9. Offline coverage for shopping lists / meal plan — currently recipe-viewing + mark-cooked only.
  10. Moderator-scoped admin routes — closed 2026-07-21 (reports + recipe-unpublish).

Deliberate design choices (not gaps)

  • Direct messaging is 1:1 only — enforced at the schema level, not a missing feature.
  • Feature toggles (userFeaturePrefs) are cosmetic nav-hiding only, never access control — separate from tier-based feature flags.
  • No native app — PWA-only is the stated architecture, covered well by the offline/install/push work already shipped.

Standing rule

Every time a feature ships, update this file in the same change — add/update its row, move it out of "Real gaps" if it closes one. Bug fixes to existing features don't need an entry unless they change what the feature does.