Repository navigation
release: v1.4.1 (Android sync, earlier versions of a recipe, security fixes) - #83
Merged
Merged
Conversation
…gging Three fixes for the "auto-share on but recipes not appearing to members / owner sees 0 shared" bug: - auto_shared_count SQL was COUNT(*) over recipe_shares rows tagged with the kitchen. With N members that double-counted (recipes * members). Now COUNT(DISTINCT recipe_id) so the label finally means "recipes shared" not "share grants". - New POST /:id/auto-share/resync endpoint. Re-runs the fan-out from the current member set. Idempotent (INSERT OR IGNORE) so its safe to click anytime. Safety valve for the "toggle-on but nothing shared" state - covers races and any silent state where the original fan-out did not complete cleanly. Requires auto_share=1 on the caller so it cant be used to bypass opt-in. - Fan-out helper now logs each run (user + kitchen + recipes counted + grants attempted + rows inserted). Makes the next occurrence of the bug diagnosable from server logs. - Client SettingsKitchens gets a small "Resync now" link under the auto-share hint, visible only when auto-share is on. Toast reports how many recipes were re-shared and how many new grants got inserted so the user knows whether anything was actually missing.
Native-app recipe creates flow through POST /api/sync/push, not POST /api/recipes. The old _autoShareNewRecipe helper was defined inline in recipes.js and only called from the REST create paths, so every recipe an Android user created got INSERTed on the server without ever triggering the Kitchen fan-out. Members never saw those recipes and the auto_shared_count stayed 0 for anything created on mobile - exactly the symptoms reported. Fix: - Extracted the helper to server/lib/auto-share.js so its home is shared between the two insert paths that exist today (and any future paths). - sync.js now calls autoShareNewRecipe(u, serverId) after every recipes-table insert in POST /sync/push. try/catch around it so a fan-out hiccup cant kill the sync response. - recipes.js imports the shared helper; local copy deleted. - Logging left in place (per-run info line) so future misfires are visible in server logs. The 78afc0b Resync button + count fix stay in place; Resync is still the recovery path for the existing bad state on your db. Going forward every insert path is covered.
The Resync surface was a safety valve added while the root cause was unknown. Now that fa153b1 wired the /sync/push path to call autoShareNewRecipe, every insert path is covered and no ongoing resync is needed. Off/on already re-fans anyway (kitchens.js:97-107 runs the fan-out on toggle-enable, INSERT OR IGNORE so its idempotent and only fills missing grants), so users who ever do need to backfill can just toggle auto-share off then on. Cleaner UI. Server logging in server/lib/auto-share.js stays so any future misfire is diagnosable.
…etons) Three small changes, mobile behavior unchanged: - .search gets max-width: 520px so the search input doesnt stretch to a ~1900px slab on a 1920px monitor. Applies to both the recipes-tab and shared-tab search rows since they share the class. - At >=1600px the recipe .grid drops from minmax(260px) to minmax(220px) and .cookbook-grid drops to minmax(240px) so 2-3 more cards fit per row on ultrawide monitors. Same pattern Pantry uses. - Loading skeleton count: 8 -> 12. On ultrawide the grid fits 8-11 cards per row so 8 skeletons was less than one full row. Still fine on phones (6 rows tall at 2 cols).
Users can now attach specific ingredients to each cooking step and Cook Mode renders them inline below the step text - no more scroll- back-to-top to see quantities. Data model (no server or schema change - JSON columns are opaque): - Each ingredient item gets a stable id (crypto.randomUUID, stamped on add + backfilled on editor load for legacy recipes). - Each step gets an optional refIds: string[] pointing at ingredient ids. Only persisted when non-empty so legacy recipes keep the identical on-wire shape. - Cleaner in save() prunes any dangling refIds pointing at removed ingredients so the JSON never carries broken pointers. Editor (RecipeEditor.svelte): - Backfill/stamp helper _newIngId. _blankIngredient stamps one. - Load hydrates existing recipes with ids + empty refIds so the chip picker has something to point at from the first edit. - Per-step .step-links panel below the step text: compact chip strip of every named ingredient across all groups. Chips are toggles - active chip shows a check mark. Only renders when the recipe has at least one named ingredient. Label shows N ingredients linked. - save() preserves id (mirror of the existing pantry_item_id line) and refIds (only when non-empty, filtered against surviving ids). View + Cook Mode (RecipeView.svelte): - _resolveStepIngs(refIds) walks recipe.ingredients and returns the linked items in refIds order, silently dropping any dangling id. - Inline <ul class=step-ings> lands inside .step-body between the step heading and the step image so quantities read as part of the step context. Cook mode scales to 16/1.5 to match surrounding step text at arms length. - Top ingredients list stays exactly as-is per the issue: unlinked ingredients still appear there. Nothing filters that section. Bonus fix (spotted while rewriting the same blocks): step imgUrl was set by the step-photo picker but never hydrated on load or persisted on save, so step photos were silently dropped on every edit. Now preserved through both round-trips. filter() at the end of save() also keeps steps that have only a photo (no title or text) which matches user intent. Compat: legacy string-only steps still load (upgrade to object shape on edit). Import paths (Paprika/Mealie/Tandoor/URL/paste) unchanged - new fields ride through the passthrough _toStorage. Sync, full backup, and native SQLite mirror all treat the columns as opaque JSON so nothing else needs touching.
The window.history.back() approach from f085526 was letting users land on the wrong page or hit a close-loop, likely from interaction with the App.svelte {#key $location} page-transition remount and iOS hash-history quirks. Reverting to a plain push(/recipes) — nav is predictable regardless of where the recipe was opened from. A route-history store is the correct way to bring back "close returns to Diary / Cookbook / etc" without touching history.back(). Deferred to a future pass.
…ient sync Three fixes on top of the issue #40 feature: - Step Photo modal: adds a Cancel button + an X icon in the header row. Backdrop-click already closed it but there was no visible affordance for either. - Editor step-link panel: collapsed by default (was permanently expanded and hogged screen space on every step). Header row shows the count and a chevron; tap expands with a soft slide. Any step that already has linked ingredients auto-expands on load so the user sees the current state at a glance. - Cook Mode inline ingredients: tap-to-check now toggles the SAME ingChecks state as the top ingredients list. Check off "1 cup rice" inline under Step 3 and it strikes through in the top list too - and vice versa. Uses the same "gi-ii" key so both surfaces stay in sync automatically. Non-cook-mode stays read-only. Visual: checked inline ingredients get strike-through + muted color matching the top list treatment. Whole strip becomes a button target in cook mode with a check-box icon on the left.
… convention Existing .cat-modal instances in RecipeEditor (Pantry picker, New Category) use Cancel + Primary buttons and no header X. My step-photo modal was the odd one out. Removed the X + the step-photo-modal-head + step-photo-close CSS so the modal shape is uniform across the editor. Backdrop click still closes.
You were right on the callback — I only checked .cat-modal instances before. Broader grep confirms the dominant pattern across CT is header-with-X, no separate Cancel: - Shopping.svelte (edit, aisle, add-from-recipe) - Sheet.svelte (shared bottom sheet) - CookbookImportDialog / CookLogDialog / ImportUrlDialog - RecipeEditor page-header The .cat-modal Cancel+Primary shape is the minority (Pantry picker, New Category) where explicit commit-vs-cancel is the point. Step photo is a pick-and-close, not a commit-vs-cancel — X in header is the right call. Kept a single primary Done at the bottom for the happy path. Backdrop click still closes as before.
…dients used Three fixes on top of the recipes wide-screen pass: - Reverted @media (min-width: 1600px) density bump on .grid / .cookbook-grid. User preferred the original 260px card min at every viewport - the tighter 220/240px cards read as underweight on ultrawide. - Reverted the .search max-width: 520px cap. Sitting alone on the left of a 1920px viewport looked isolated. Back to full-bleed which matches the sticky-controls chrome around it. - toggleStep in cook mode now cascades: checking a step done also marks its linked ingredients (refIds) as used. Users who cook straight through without checking each ingredient individually get the same end state. One-way only - unchecking a step leaves ingredients alone since they may belong to other steps too.
Tandoor exports carry step.ingredients[] scoped to each step. The importer already grouped them by step name into the ingredient groups, but built the CT steps array separately from just s.name + s.instruction — throwing away the per-step adjacency. Now: each imported ingredient item gets a stable uuid at construction time. A WeakMap maps source step → id list. When building the CT steps array, refIds gets attached from that map so the linked- ingredients-per-step feature works out of the box for anyone migrating from Tandoor. Existing user recipes untouched (the id + refIds fields only appear on newly-imported Tandoor recipes). Additive change only: - Adding id to ingredient item: server _toStorage passes through unchanged (audit confirmed no allowlist / stripping). - Adding refIds to step: same. Only persisted when non-empty. - Steps still get filtered by (s.text) so ingredient-only source steps still contribute their ingredients to the top list even when they have no instruction to render. Other importers unchanged. Paprika / Mealie / URL / paste dont carry per-step ingredient info in their source formats so theres nothing to preserve there.
…t removal Enabling user management only re-pointed a subset of tables, so data written while the instance had no accounts silently stopped being visible. routes/oidc.js carried its own shorter copy of the same list. Both now call one shared claimAnonymousData(), and startup adopts anything an older build already stranded when exactly one account exists. Account removal had the mirror problem: tables with no FK to users(id) survived the cascade, leaving wearable history and live OAuth refresh tokens behind. All four removal paths now clear them. Tests derive the expected tables from db.js so a new one cannot slip past.
…rigin (#42) Behind a TLS-terminating reverse proxy req.protocol reflects the internal socket (http), not the client scheme (https). Forgot-password links, invite links, and the SMTP test emails embedded logo origin all rendered as http://, which mail providers flag as spam. oidc.js already uses the correct x-forwarded-proto / x-forwarded-host pattern. Applied the same pattern to auth.js and app-config.js to match. Co-Authored-By: Clifton Molina <[email protected]>
… EnvHttpProxyAgent + globalThis.fetch swap so every outbound request routes through a configured forward proxy without per-call-site changes
File / paste / bulk import dialogs were calling fetch(/api/recipes/...) directly instead of going through NtApi._fetch, so the X-CSRF-Token header was never attached. On PWA (cookie sessions) csrfProtect returned 403 Invalid CSRF token. Native (Bearer) was CSRF-safe so it worked there — this was PWA-only. Fix: exported mutatingAuthHeaders() from src/lib/api.js and threaded it through every raw-fetch mutating call on the four import dialogs: - FileImportDialog: extract-and-parse (x2) - CookbookImportDialog: extract-and-parse - ImportUrlDialog: batch-scrape, bulk-commit, bulk-cancel - BulkFileImportDialog: bulk-scan, bulk-commit, bulk-cancel Helper picks Bearer on native / X-CSRF-Token on PWA the same way NtApi._fetch does, so future raw-fetch callers can drop it in without rediscovering the wire. bulk-text is a GET (no method) and stays as-is - GETs are on the CSRF middleware safe-methods list. Verified with npx vite build. Server-side unchanged.
… land (port from LT #68)
…total-polyunsaturated (was mapping poly to the mono total and mono to a single fatty acid, oleic 18:1, so imported foods had a nonsensical fat split). Port of TraceApps/nutritrace#179. Co-Authored-By: herver1971 <[email protected]>
…l miss Only owned recipes ride the sync-pull into local SQLite. Shared recipes (Kitchen fan-out, direct grant, cookbook grant) live only on the server. On native connected mode getRecipe dispatched through CtApiCached → CtApiNative → local lookup → returned null for any shared id → RecipeView rendered blank. CtApiCached now wraps getRecipe: local lookup first (owned recipes stay fast and offline-capable), on miss falls back to a server fetch with the same Bearer-auth pattern _CtApiHttp uses. Also bumps to v1.2.0-dev.02 and logs the fix.
Server /api/recipes/:id returns rows with img_url; RecipeView reads
recipe.imgUrl. The initial fallback commit forwarded the raw server
response, so shared recipes opened but the header image (and any
img_url-derived paths downstream) rendered against undefined.
Apply the same {img_url, ...rest} → {imgUrl: resolveAssetUrl(img_url)}
transform _imgFromApi does on the standard HTTP path so both surfaces
hand RecipeView an identical shape.
…s missing a scheme (port of TraceApps/nutritrace#177 follow-up)
Three pages left dead space on wide monitors: - RecipeView .body capped at 1440px (>=1280 tier) with nothing beyond. Added a >=1600px tier at 1760px. - RecipeEditor .form-wrap capped at 1480px (>=1400 tier) with nothing beyond. Added a >=1700px tier at 1760px. - CookbookView .editor-content had a FLAT 1180px cap at every viewport width, no scaling tiers at all - same class of bug Manage.svelte had before its wide-screen pass. Its .grid is already repeat(auto-fill, minmax(220px, 1fr)) so it flows more columns automatically once the wrapper has room; added >=1280px (1440px) and >=1600px (1760px) tiers. All three converge on the same 1760px ceiling on a 1920px monitor, consistent with the density work already done on Pantry/Diary/ Shopping/Manage.
Cookbooks had no way to find a recipe once they grew past a screen or two - no search, no sort. Added both, scoped carefully around the existing manual drag-reorder feature: - Search input + sort dropdown (Manual Order / A-Z / Favorites First) above the grid, shown whenever a cookbook has >1 recipe. - displayRecipes is a view-only derived list - it never touches cookbook.recipes, the array the up/down reorder buttons persist against via NtApi.reorderCookbookRecipes. - Reorder buttons (up/down arrows) only render when the visible list IS the true manual order (no search, sort=Manual). A search or non-manual sort makes the loop index no longer correspond to the recipes real position in cookbook.recipes, so showing the arrows there would silently reorder against the wrong neighbors. Remove and move-to-cookbook buttons stay available always - both are id-based, never index-based, so they are safe under any view. - No-matches empty state added for the search-active + zero-results case. Deliberately NOT gated on is_smart - smart cookbooks can also be searched and need the same empty-state feedback. - Smart cookbooks hide the sort dropdown (their order is computed by the saved filter, not draggable) but keep the search input.
Two follow-ups from user feedback on the search/sort pass: - Replaced the up/down arrow reorder buttons with actual drag-and- drop, using svelte-dnd-action (already a dependency, same pattern Shopping.svelte uses for its aisle groups). Grip handle sits at the bottom-right of each card so the rest of the card stays the click target to open the recipe. Only active when cbReorderable (no search, sort = Manual) - same constraint as the old arrows, now enforced via dragDisabled instead of hiding buttons. dragHandleZone binds to displayRecipes (not cookbook.recipes directly) so the items count always matches the actual rendered DOM children, avoiding a mismatch when filtered/sorted (drag is disabled then anyway, but this keeps the librarys internal bookkeeping honest). consider updates cookbook.recipes live for drag feedback; finalize persists via the same NtApi.reorderCookbookRecipes call the old buttons used. - Cookbook cover image is now settable. The server already had full cover_image_url support on create/update (server/routes/ cookbooks.js) - there was just no client UI for it. Clicking the cover thumbnail (owner, non-smart cookbooks only) opens a picker sheet reusing the shared ImagePicker component and the same header-X modal shell the Step Photo picker uses in RecipeEditor. Hover overlay with a camera icon signals its clickable. Also confirmed: the "N recipes" line under the cookbook name is body/stat text, correctly sentence-case per house style already - no change needed there.
FLIP-based reorder libraries (svelte-dnd-action here) animate every card position change via CSS transform on each consider event. Without compositor hints the browser was re-layouting/repainting every image-heavy card from scratch each frame instead of doing pure GPU transform work - reads as choppy, especially on Android WebView where GPU compositing headroom is tighter than desktop Chrome. - .recipe-card gets will-change: transform (promotes each card to its own compositor layer up front) + contain: paint (isolates each cards repaint cost so animating one doesnt force the browser to reconsider its neighbors). - The actively-dragged card (id-targeted via the librarys DRAGGED_ELEMENT_ID = #dnd-action-dragged-el) gets pointer-events: none on its contents so hover/focus recalculation doesnt compete with the drag gesture for frame budget, plus a lifted box-shadow so the motion reads as physical rather than a flat teleport. flipDurationMs stays at 180 (matches Shopping.svelte) - duration was not the issue, frame cost was.
…ragged one
The dragged card itself moved smoothly (svelte-dnd-action handles
that), but every other card snapped/popped straight to its new grid
position instead of sliding - because animating the rest of the
list during a drag is not something svelte-dnd-action does on its
own. Its flipDurationMs option only informs its own internal
calculations; the library expects the consumer to attach Svelte's
own animate:flip directive to each list item for the make-room
animation. That was missing.
Svelte requires animate: to sit on the single direct child of a
keyed {#each} block, but a cookbook recipe renders one of three
different top-level elements (drag placeholder / locked card /
normal card) depending on state - so the each block cannot put
animate:flip on each branch individually (confirmed by a compile
error: "must be the only child of a keyed {#each} block"). Fixed by
wrapping all three branches in one transparent .cb-grid-item div
that IS the single direct child, carrying animate:flip with
duration: FLIP_MS. No box styling of its own so .recipe-card inside
still looks and sizes exactly as before.
Follow-on corrections since dnd-action-dragged-el now lands on the
new wrapper instead of directly on .recipe-card:
- Moved the will-change: transform / contain: paint compositor
hints from .recipe-card to .cb-grid-item, since that is the
element actually receiving the transform now (both the drag
library's own animation and Svelte animate:flip apply to it).
- The lifted drag-shadow now targets the .recipe-card nested inside
the dragged element specifically (matches the card's rounded
corners - the wrapper itself has none).
- contain: paint clips anything painted past an element's own
border-box, which would have clipped that shadow since the
wrapper is sized identically to its child. Overridden to
contain: none on just the actively-dragged item so its shadow
renders in full; every other card keeps the containment
optimization, which is where the reflow jank actually was.
A recipe's card inside a cookbook was noticeably thinner than the same recipe's card on the Recipes tab - no category badge, no star rating, no tags, no pantry-match pill. Root cause was server-side: GET /api/cookbooks/:id pulled a narrow, hand-picked column list for each recipe and never ran them through hydration at all, while GET /api/recipes (and every other recipe-card endpoint) resolves category, parses tags/ingredients, and computes pantry_match. Two independent implementations that had quietly drifted apart - the same class of bug as the Kitchen auto-share fan-out miss earlier this session (server/lib/auto-share.js). Extracted the shared hydration into server/lib/recipe-hydrate.js: hydrateRecipe, normaliseIngredientGroups, safeJson, matchSummary, buildStockSet, buildCategoryMap. recipes.js now imports these aliased to its original local names (_hydrate, _safeJson, etc) so every existing call site in that large file is unchanged; its own local copies of these functions are deleted. Its GET / handler's inline stock-set/category-map building blocks are replaced with calls to the same shared builders cookbooks.js now also calls - removes ~35 lines of duplicated logic and guarantees both endpoints compute pantry_match and category identically by construction. cookbooks.js changes: - Non-smart branch's recipe SELECT widened from a dozen named columns to r.* (+ link_order), matching what hydrateRecipe expects. - _evalSmartFilter (smart-cookbook branch) also widened to r.*, and no longer strips tags before returning - it was fetching tags just to filter on them in JS, then discarding them. - GET /:id builds a category map scoped to the cookbook OWNER (cb.user_id) - recipe.category_id always points into the owner's own category table regardless of who is reading, so this has to be the owner even for a shared-cookbook read. - pantry_match is only computed for the owner's own read. For a shared-cookbook read, "N of M you have" would have to mean either the viewer's or the owner's pantry - genuinely ambiguous - and GET /recipes/shared-with-me already resolves the same ambiguity by omitting pantry_match entirely rather than guessing. Matched that existing precedent instead of inventing a new answer. Client: CookbookView.svelte's card markup now renders the fields that were already being silently dropped - category badge (with the category-color left-border stripe the Recipes tab cards have), star rating, up to 2 tags, and the pantry-match pill. Copied the exact markup + CSS from Recipes.svelte's own card for visual parity.
…lready have opens it
…ack for the installed app (cherry picked from commit 63fe776)
(cherry picked from commit ec2f537)
(cherry picked from commit 12d6187)
…AI, push, NutriTrace link (cherry picked from commit 1a4ee1a)
…pgrades (cherry picked from commit 060776f)
… for push and NutriTrace link; checked /api/proxy (cherry picked from commit f51fc71)
(cherry picked from commit 569d37c)
…ever the request's host (cherry picked from commit ce8f328)
(cherry picked from commit c38b1cc)
…ocks script and follows recipe sharing (cherry picked from commit f39da1f)
…as a page (cherry picked from commit a060af2)
(cherry picked from commit 11f0cc2)
Co-authored-by: Lionel Penaud <[email protected]> Language: French Translate-URL: https://hosted.weblate.org/projects/cooktrace/app/fr/ Translation: CookTrace/App (cherry picked from commit db90423)
Co-authored-by: defly91 <[email protected]> Language: Italian Translate-URL: https://hosted.weblate.org/projects/cooktrace/app/it/ Translation: CookTrace/App (cherry picked from commit 7b8bf8d)
…newer edit wins with the other copy kept as a version (cherry picked from commit 3af0dfa)
…dient pantry links stay right between phone and server (cherry picked from commit 49090fa)
Co-authored-by: defly91 <[email protected]> Language: Italian Progress: 1.1% (17 of 1448 strings) Translate-URL: https://hosted.weblate.org/projects/cooktrace/app/it/ Translation: CookTrace/App (cherry picked from commit b51e71f)
…, source-map-js, @modelcontextprotocol/sdk and proxy-addr (cherry picked from commit 6651906)
(cherry picked from commit 52d2d29)
…those already stored inline into files (cherry picked from commit 88f6284)
Version 1.4.1 (Android 106, watch 3), the consolidated 1.4.1 changelog, and vite 7.3.7 and hono 4.13.13 so the release has no known advisories.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CookTrace 1.4.1: the 1.4.1 hotfix line (v1.4.1-dev01 to dev03) promoted to main, with vite and hono bumped so it releases with no known advisories. Full notes in CHANGELOG.md.