docs: add code-verified feature audit (FEATURE_AUDIT.md)
Full read of routes/schema/components across recipes+AI, meal planning/pantry/shopping/nutrition, social/messaging/notifications, admin/billing/ops, and PWA/offline/mobile -- corrects several wrong assumptions from earlier speculation (recipe-import-from-URL, pantry barcode scan, and cooking-mode voice control all already exist; they were guessed as missing before this audit). Real confirmed gaps: no Stripe checkout/billing-portal (only the webhook consumer exists), no recipe video, no nutrition trend view, no OS share-target, no push click-through handling, no anonymous meal-plan link, no offline coverage beyond recipes+mark-cooked. Per user request, this file should be kept current going forward -- every shipped feature gets an entry here alongside the usual version-bump/changelog/OpenAPI updates.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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 | Mon–Sun 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 (collaborators only) | `meal_plan_members` viewer/editor roles; **no anonymous public link** (unlike shopping lists) | `apps/web/lib/meal-plan-access.ts` |
|
||||
| **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 | Partial | Single calendar day only — **no multi-day trend/history chart** | `apps/web/app/api/v1/users/me/nutrition-diary` |
|
||||
| **Barcode/USDA nutrition-facts database** | **Missing** | Barcode scan only IDs the product name, doesn't pull nutrition facts; all nutrition numbers are AI-estimated, never database-sourced | — |
|
||||
| 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 click-through handling | **Missing** | No `notificationclick` listener in the service worker — the `url` field encoded in push payloads isn't consumed client-side | `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 | Partial | Enum value exists (`user`/`moderator`/`admin`) but no moderator-scoped route was found — report review currently requires full admin | `packages/db/src/schema/users.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) | **Missing** | No `share_target` in the manifest | — |
|
||||
| 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** — diary is single-day only, no multi-day chart.
|
||||
4. **USDA/nutrition-database lookup** — all nutrition numbers are AI-estimated; barcode scan only gets product name, not nutrition facts.
|
||||
5. **OS Share Target** — can't share a recipe link into Epicure from another app's share sheet.
|
||||
6. **Push click-through handling** in the service worker — payload carries a `url`, nothing consumes it.
|
||||
7. **Anonymous public link for meal plans** — shopping lists have this, meal plans don't.
|
||||
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** — role exists in the schema, not wired to any route.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user