Manual: DELETE /api/v1/ai/chat-history (scoped by recipeId or scope=general,
matching GET's scoping) plus a trash-icon button + confirm dialog in both
chat panels' headers.
Automatic: new internal cron endpoint (chat-cleanup), same shared-secret
pattern as the existing leftover-reminders/weekly-digest crons, deletes
any chat_messages older than 90 days. Wired into cron/crontab (daily,
03:00 UTC) and the Dockerfile's cron stage.
Verified locally: cleared a real conversation through the actual UI and
confirmed it didn't come back on reopen (not just cleared client-side);
inserted a 100-day-old and a 5-day-old message directly, called the cron
endpoint with the real shared-secret check, confirmed only the old one
was deleted and the recent one survived; confirmed the endpoint 401s with
no/wrong secret.
getPublicUrl() runs in the browser (called from client components rendering
recipe thumbnails), but read the plain STORAGE_PUBLIC_URL env var — never
inlined into the client bundle, so every browser fell back to the hardcoded
localhost:9000 default regardless of the real deployed storage domain,
tripping CSP img-src and mixed-content blocks in production. Added a
NEXT_PUBLIC_STORAGE_PUBLIC_URL build arg (Dockerfile, compose.prod.yml) wired
from the same STORAGE_PUBLIC_URL value, and getPublicUrl() now reads that.
Verified locally: building with a fake public storage domain set shows it
correctly inlined into the client JS chunk (previously only the localhost
fallback ever appeared there).
- Presigned upload URLs were signed against STORAGE_ENDPOINT (the
internal docker hostname, e.g. http://minio:9000) instead of a
browser-reachable URL — uploads/edits with photos were broken in
prod. Now signed with a separate client bound to STORAGE_PUBLIC_URL,
which also needed threading through as a Docker build arg (CSP
headers are computed at build time) and into compose.prod.yml/
.env.production (gitignored, not committed).
- recipe-form.tsx used the wrong i18n namespace for the visibility
label (t("recipeForm") instead of t_recipe("recipe")), causing
MISSING_MESSAGE in French.
- Compact recipe-list view: batch-cook rows showed no dish count, and
the date/difficulty columns shifted per-row depending on whether a
difficulty badge was present — reserved a fixed-width slot for it.
- All three card icon badges (batch-cook, favorite, visibility) now
share the same muted color instead of the batch badge standing out.
- Recipes filter dropdown: clicking a filter option closed the whole
menu, and the tag text input couldn't be typed into (the menu's
roving-focus/type-ahead handling was intercepting keystrokes) — menu
items now stay open on click, and the tag input stops event
propagation so it behaves like a normal text field.
- Recipe form: added a floating save/cancel bar so long recipes don't
require scrolling back down to save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Shopping list add already worked generically for batch-cook recipes —
no code needed there.
New: mark a specific batch-cook dish as "cooked today", track its
fridge expiry (cookingHistory.batchDishId), surface a "Leftovers
expiring soon" widget on the pantry page, and send a daily push+email
reminder via a new /api/internal/cron/leftover-reminders endpoint
(mirrors the weekly-digest cron pattern; doesn't use the social
notifications table, which requires a non-null actor and isn't built
for self-reminders).
Also fixes, from user-reported bugs:
- Recipe cards showed no batch-cook badge/dish-count/prep-time in some
views — added dishCount + prepMins/cookMins (now generated by the AI
and persisted) to the card component and /recipes query.
- Batch-cook descriptions occasionally contained raw markdown
(**bold**) — added explicit "plain prose only" prompt instructions
and a stripMarkdown() defensive fallback at render time.
- Truncated/cut-off descriptions — the generateObject call had no
maxOutputTokens set, so long structured responses could get cut off
mid-field; now capped explicitly at 8000.
- Generate dialogs (batch-cook + the main AI dialog) could show
buttons unreachable once the progress bar appeared mid-generation —
restructured so the action row is pinned outside the scrollable
content area, not affected by content height changes.
- /api/internal/* routes were being redirected to /login by middleware
before their own CRON_SECRET check ever ran (pre-existing bug,
affected the weekly-digest cron too) — added to PUBLIC_PATHS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moved compose.yml, compose.prod.yml, DEPLOY.md, traefik/, and cron/ out
of docker/ into the repo root (docker/ removed). Updated every reference:
- Dockerfile: COPY paths for the cron stage
- .dockerignore: dropped the now-unneeded docker/!docker-cron
exclude+exception pair (cron/ is just a normal top-level dir now, no
special-casing needed)
- compose.prod.yml: build context ".." -> "." (Dockerfile and compose
files are now siblings, not one level apart)
- CLAUDE.md, DEPLOY.md, compose.yml, traefik/epicure.yml,
cron/run-digest.sh, weekly-digest route.ts: path references in comments/
docs
Verified: both compose files validate, and all three Dockerfile targets
(runner/cron/migrator) build clean via a local --no-cache docker build
from the new layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Six M-sized items from HANDOFF.md's new-features backlog:
- Profile tabs: cooking-history stats (total cooked, last-cooked, streak)
and a "cooked it" photo gallery, both owner-only
- Display-time unit conversion (metric<->imperial) for recipe ingredients,
respecting each user's unitPref; original value always shown alongside
the conversion
- Nutrition daily diary: per-day macro totals computed from cooking history
x recipe nutritionData, compared against user goals
- Pantry scan: real barcode lookup (zxing + Open Food Facts, no API key)
with an AI-vision fallback for unbarcoded items, always confirm-before-
insert, both paths tier/rate-limited like other AI features
- Weekly digest email: new followers/comments/ratings + trending recipes,
sent via a new `cron` Docker stage (alpine+crond+curl) and `digest-cron`
compose service hitting a bearer-token-protected internal route
- Meal-plan generation can now target a user's nutrition goals as a
prompt-level nudge (recipes are AI-invented, not DB-sourced, so this
can't be a hard macro constraint)
Caught a real deploy-breaking issue while adding the cron stage: appending
it after `runner` silently changed the Dockerfile's default build target,
and `web`'s compose config didn't pin one — fixed by pinning `target:
runner` explicitly. Verified with typecheck, lint, and three separate
`docker build --target` runs (runner/cron/migrator) plus `docker compose
config` validation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dockerfile still COPYed packages/api-types (deleted in the prior audit-fixes
commit), breaking Portainer builds. Also drop apps/web/middleware.ts, added
in the same commit for auth guarding — it duplicated and conflicted with the
pre-existing apps/web/proxy.ts (Next 16's middleware entry point), which
already redirects unauthenticated requests to /login, 401s API routes, and
covers /recipes/new, /explore, /search, /settings/webhooks/docs via its
catch-all matcher. Having both middleware.ts and proxy.ts breaks `next build`
outright. Verified with a local `docker build` end to end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NEXT_PUBLIC_VAPID_PUBLIC_KEY (and the OAuth *_ENABLED flags) are inlined
into the client bundle by next build — setting them as container env vars
at runtime does nothing, since the client JS was already built without
them. Client bundle shipped with applicationServerKey: undefined, so
pushManager.subscribe() threw and the button always failed with "Failed
to enable notifications."
Wire them as Docker build args (Dockerfile ARG/ENV before `next build`,
compose.prod.yml build.args), sourced from the same stack env vars used
at runtime so they stay in sync.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a migrator Dockerfile target (drizzle-kit migrate + tier-definitions
seed, both idempotent) and wire it as a one-shot compose service that web
waits on via service_completed_successfully. No more manual exec step
after first deploy — runs safely on every redeploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add root Dockerfile (standalone Next output, multi-stage pnpm build), drop
in-stack caddy in favor of publishing web's port for an external traefik
LXC (file-provider dynamic config included), and document the portainer
deploy flow.
Also fixes issues that blocked any production build: a bad auth-client
type cast, the ai SDK's mimeType->mediaType rename, an implicit-any
callback param, and push.ts eagerly calling webpush.setVapidDetails at
module import time (which crashed page-data collection whenever VAPID
env vars weren't present at build) — now lazily configured on first send.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>