Skip to content

release: v1.4.1 (Android sync, earlier versions of a recipe, security fixes) - #83

Merged
thebigjoe1 merged 408 commits into
mainfrom
release/1.4.1
Oct 22, 2026
Merged

thebigjoe1 merged 408 commits into
mainfrom
release/1.4.1

Conversation

@thebigjoe1

Copy link
Copy Markdown
Contributor

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.

thebigjoe1 and others added 30 commits August 23, 2026 11:10
…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.
…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.
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.
thebigjoe1 and others added 28 commits October 3, 2026 18:59
…ack for the installed app

(cherry picked from commit 63fe776)
…AI, push, NutriTrace link

(cherry picked from commit 1a4ee1a)
… for push and NutriTrace link; checked /api/proxy

(cherry picked from commit f51fc71)
…ever the request's host

(cherry picked from commit ce8f328)
…ocks script and follows recipe sharing

(cherry picked from commit f39da1f)
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)
…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.
@thebigjoe1
thebigjoe1 merged commit f2734f0 into main Oct 22, 2026
1 check passed
@thebigjoe1
thebigjoe1 deleted the release/1.4.1 branch October 22, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants