Skip to content

Fix recipe text loss, calendar overflow on phones, and silent failures - #1

Merged
weemsr merged 4 commits into
mainfrom
claude/repo-review-improvements-i00kge
Aug 21, 2026
Merged

weemsr merged 4 commits into
mainfrom
claude/repo-review-improvements-i00kge

Conversation

@weemsr

@weemsr weemsr commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Four commits from a repo review, each verified against the real thing rather than by inspection.

Recipes

  • A bare < ate the rest of the line. The HTML strip used /<[^>]*>?/gm, whose optional closing > meant "cook until temp is <165 F" rendered as "cook until temp is ". It ran at display and when opening the editor, so saving persisted the truncation.
  • Editing certain imported recipes wiped them. Recipes whose stored ingredients were objects rather than strings were dropped silently: the recipe showed "No ingredients listed", the editor opened blank, and saving replaced the real data with []. Now the object shapes are recovered, and a save can never overwrite stored content with an empty list.
  • No way out of edit mode. Only Save was offered, and Save always writes. Added Cancel.
  • &#32; / &#39; showing as literal text. Sites put numeric HTML entities inside JSON-LD strings; JSON isn't HTML, so nothing decoded them. The DOM-scraping paths never hit this because cheerio's .text() decodes for us. Also tidies WP Recipe Maker's doubled parens (((divided))) and double spaces.
  • Split ingredient lists were dropped. Sites that break ingredients into "For the pasta" / "For the sauce" sub-lists lost every short section — one page imported a shrimp pasta with neither shrimp nor pasta. Lists are now scored and sibling lists merged, with headings kept as section labels.
  • Import bypassed the length caps entirely; scraped titles, lines, and list length are now bounded server-side.

Calendar / layout on iPhone

MonthGrid used repeat(7, 1fr) with square cells. A bare 1fr floors each track at min-content, and for a square cell that floor comes from its height, so seven cells measured 479px against a 393px viewport — the page scrolled sideways, Saturday was clipped, and the last row escaped its card. Measured in Chromium at iPhone SE and iPhone 14 Pro widths, the document now matches the viewport exactly at both.

Also: min-height: 100vh → added 100dvh (iOS resolves 100vh against the large viewport, giving every page phantom scroll); the bottom nav grows to include the home-indicator inset instead of eating its own content height; and the nav scrolls the active tab into view, since Calendar is the 8th of 10 tabs and was off-screen with no hint it existed.

Ingredient scaler was hard to tap

.card:active { transform: scale(0.98) } applied to every card. Displacement grows with distance from the card's centre, so on Recipes — where one card wraps the whole list and an expanded recipe is ~1600px tall — the scale buttons shifted 12.7px while held, half the button's own height sliding out from under the finger. Press feedback is now scoped to a.card / button.card; only 2 of the ~22 cards in the app are actually pressable.

Silent failures

  • schema.sql never enabled Realtime. Every page subscribes to postgres_changes on items, but the table was never added to the supabase_realtime publication — so subscriptions connect, report success, and never fire. Also sets REPLICA IDENTITY FULL (delete payloads carried only the primary key) and NOT NULL on user_id, guarded so it reports rather than fails. The whole file is now re-runnable and touches no rows.
  • CSV import reported the wrong spreadsheet line for every issue after a blank row.
  • ICS folding counted UTF-16 units while promising octets, so emoji summaries broke the 75-octet limit and could be cut mid-surrogate.
  • Browser and server minted different random ids for calendar entries lacking one, so per-calendar status never matched and errors were swallowed.
  • todos rename ignored its update error, so a failed rename looked like it worked.
  • Three pages froze "today" at mount; extracted the midnight-rollover handling into a shared useToday.
  • Added a per-user throttle to the vision-scan route, the only endpoint that spends money per call.

Housekeeping

Real square PWA icons (the manifest declared the 1024×338 masthead as 192×192 and maskable), ~1.1 MB of unreferenced images removed, unused @supabase/ssr dropped, and README corrections (wrong clone directory, two Google Calendar features documented as present that have no UI, missing build-env note).

Verification

Lint clean, 111 tests pass (34 new, pinning each regression above), production build succeeds. Live re-imports confirmed against both problem sites.

After merging

Re-run schema.sql in the Supabase SQL editor — that's the one fix that lives in the database rather than the code, so merging alone won't apply it. It's guarded and re-runnable, and deletes nothing.

Already-saved recipes clean up on next view without a migration; no reimport needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GkRigAT6CpLEjV7SzKMdZq


Generated by Claude Code

claude added 4 commits August 17, 2026 03:33
Recipe editing (the reported bug):

- The HTML strip used /<[^>]*>?/gm, whose optional closing `>` made a bare
  `<` swallow the rest of the line: "temp is <165 F" rendered as "temp is ".
  It ran at display time and again when opening the editor, so saving
  persisted the truncation. Replaced with a known-tag-only pattern in
  src/lib/html.ts, shared by all three call sites.

- Recipes whose stored ingredients were objects rather than strings were
  dropped by asStringArray: the recipe showed "No ingredients listed", the
  editor opened blank, and saving replaced the real data with []. asStringArray
  now recovers text from the object shapes scrapers emit, and saveEdit refuses
  to write an empty list over stored content.

- Added a Cancel button. Edit mode previously offered only Save, which always
  writes, so a mistaken tap on the pencil had to be committed.

- The import path bypassed the length caps entirely; scraped titles, lines,
  and list length are now bounded server-side.

Silent failures:

- schema.sql never added `items` to the supabase_realtime publication, so
  every page's live-update subscription connected, reported success, and
  never fired. Also sets REPLICA IDENTITY FULL (delete payloads carried only
  the primary key) and NOT NULL on user_id, guarded so it reports rather than
  fails. Whole file is now re-runnable.

- CSV import reported the wrong spreadsheet line for every issue after a blank
  row, because blank rows are dropped during parsing. Rows now carry their
  original line number.

- ICS folding counted UTF-16 code units while promising octets, so emoji
  summaries exceeded the 75-octet limit and could be cut mid-surrogate.

- Browser and server minted different random ids for calendar entries without
  one, so per-calendar status never matched and errors were swallowed.

- todos saveEdit ignored its update error; a failed rename looked like it
  worked.

Other:

- maintenance, credit-cards, and calendar froze "today" at mount, so a tab
  left open overnight showed stale overdue counts. Extracted the rollover
  handling meals already had into useToday and shared it.

- Added a per-user throttle to the vision-scan route, the only endpoint that
  spends money per call.

- PWA icons declared the 1024x338 masthead as 192x192/512x512/maskable;
  generated real square icons. Removed ~1.1MB of unreferenced images and the
  unused @supabase/ssr dependency.

- README: wrong clone directory, two Google Calendar features documented as
  present that have no UI, and no mention that `npm run build` requires the
  Supabase env vars.

Verified: lint clean, 89 tests pass (12 new covering these regressions),
production build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkRigAT6CpLEjV7SzKMdZq
…t lists

Calendar month grid (reported: "doesn't fit on the iPhone screen"):

- MonthGrid used `repeat(7, 1fr)` with square cells. A bare `1fr` floors each
  track at min-content, and for a square cell that floor comes from its
  *height* (~55px), so seven cells plus gaps measured 479px against a 393px
  viewport. The whole page scrolled sideways, Saturday was clipped, and the
  last row escaped its card. Switched to minmax(0, 1fr) plus minWidth: 0 on the
  cells; measured in Chromium at iPhone SE and iPhone 14 Pro widths, the
  document now matches the viewport exactly at both.

- Tuned the cell contents for the resulting ~41px box: two event bars instead
  of three, and clamp() font sizes for the date and overflow count.

- .container used min-height: 100vh, which iOS Safari resolves against the
  large viewport, so every page kept a strip of phantom scroll. Added 100dvh
  with the 100vh left as fallback.

- The bottom nav folded env(safe-area-inset-bottom) into padding while
  box-sizing is border-box, leaving ~46px of usable height on a home-indicator
  iPhone. It now grows the box instead, and body padding-bottom matches.

- Ten destinations cannot fit a phone, so the nav scrolls — but that left
  Calendar (8th) off-screen with no hint it was there. It now scrolls the
  active tab into view on navigation, and tighter tab metrics show ~2 more per
  screen. Restructuring to fewer primary tabs is still worth considering.

Recipe importer (foodiefiber.com):

- That site publishes schema.org Article only — no Recipe — so extraction falls
  to the DOM heuristic, which took the first list with more than two items.
  The page splits ingredients into "For The Pasta" (1 item), "For The Shrimp"
  (1 item) and "For The Sauce" (10), so a shrimp pasta imported with neither
  shrimp nor pasta and nothing signalling the loss.

- Replaced with per-list scoring (a majority of lines must lead with a quantity
  or carry a unit word) plus merging of sibling lists that share a parent, which
  is how one recipe's sections are marked up. The heading above each list
  becomes a section label, matching the Sanity path's formatting. Verified
  against the live page: 15 lines, shrimp and pasta present, nav menus and the
  prose tips list correctly rejected.

- Moved the helper to src/lib/recipe-extract.ts so it can be unit tested;
  "use server" modules may only export async functions.

Note: that page carries no serving count in any form, so ingredient scaling
stays unavailable for it regardless of extraction.

Verified: lint clean, 100 tests pass (11 new), production build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkRigAT6CpLEjV7SzKMdZq
Pressing any button inside a .card put the whole container into :active, and
`transform: scale(0.98)` moved everything in it. Displacement grows with
distance from the card's centre, so on the Recipes tab — where one .card wraps
the entire list and an expanded recipe is ~1600px tall — the scale buttons sit
far enough from centre to shift 12.7px while held, animated over 0.2s. That is
half the button's own height sliding out from under the finger mid-press, which
made the 1x/1.5x/2x control unreliable to hit.

Measured in Chromium at iPhone 14 Pro width, pressing the "2x" button:

  before  y=194.7  ->  while held  y=207.4   (12.7px, transform: matrix(0.98…))
  after   y=194.7  ->  while held  y=194.7   ( 0.0px, transform: none)

Only 2 of the ~22 cards in the app are interactive — the home dashboard tiles,
which are links — so the feedback is now scoped to `a.card` / `button.card`.
Those still scale on press; content containers no longer move. The transform
transition is scoped the same way.

Also added touch-action: manipulation to the scale buttons. RecipeDisplay was
the only recipe component without it, so those buttons still carried the 300ms
tap delay and double-tap-to-zoom.

Verified: lint clean, 103 tests pass (3 new), production build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkRigAT6CpLEjV7SzKMdZq
justonecookbook.com/japanese-beef-curry imported with visible markup in almost
every step — "Cut 2&#32;onions into thin slices.", "When it&#39;s hot" — and
mangled ingredient lines like "2  onions ((large; 1¼ lb, 567 g))".

Both come from the source, not from our processing (checked: the raw JSON-LD
holds 42 literal &#32; and 8 &#39;, and zero &amp;#32;, so we are not
double-encoding).

- Entities: the site puts numeric HTML entities inside JSON-LD strings. JSON is
  not HTML, so JSON.parse leaves them as visible text and nothing downstream
  decodes them. The DOM-scraping paths never hit this because cheerio's .text()
  decodes for us, which is why it went unnoticed. Added decodeEntities() for
  numeric (decimal and hex) plus the named set recipe sites actually emit,
  leaving unknown or out-of-range entities untouched rather than replacing them
  with a placeholder glyph.

- Artefacts: "2  onions ((large; 567 g))" is verbatim WP Recipe Maker output —
  it emits a double space when a quantity has no unit, and wraps an already
  bracketed note in another pair. Deterministic, so cleanRecipeLine() collapses
  doubled parens and runs of whitespace. Genuinely nested parens are left
  alone.

Applied at the import boundary and again at render, so recipes already saved
with entities in them clean up without a data migration.

Live re-import of that page: all 24 ingredients and 27 steps now read as plain
text. foodiefiber still returns its 15 lines, unchanged.

Verified: lint clean, 111 tests pass (11 new), production build succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkRigAT6CpLEjV7SzKMdZq
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
famlihub Ready Ready Preview Aug 21, 2026 1:30pm

@weemsr
weemsr merged commit 148d910 into main Aug 21, 2026
3 checks passed
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.

2 participants