wanessalabs standalone production July 29, 2026

Recipe Flow

Recipes tell you the steps and never when to start. This one schedules backward from your serve time and shows what combines with what.

ReactTypeScriptViteTailwindZodDexieTanStack QueryHonoVitestCloudflare Pages
What combines with what, and when to start so everything lands at once.
What combines with what, and when to start so everything lands at once.
Built for arm’s length on a tablet, with the merge chart under the ingredients.
Built for arm’s length on a tablet, with the merge chart under the ingredients.

Every recipe page buries a twelve-line recipe under ads and a nine-hundred-word story about a farmhouse kitchen. Stripping that out is table stakes and a dozen tools already do it. The interesting problem is the one underneath: even a perfectly clean recipe is a flat numbered list, and a flat list hides the two things a cook actually needs.

A Recipe Has Two Structures, and a List Shows Neither

What combines with what. “Cut the butter into the dry ingredients” assumes you know which of the seven ingredients above are the dry ones, and that they were already combined. That is topology, and it renders as a merge chart: ingredients as rows, each operation spanning the contiguous block of rows that feed it.

When to start. Every step is written as though you begin it the instant the previous one ends, which is false. Bread rises for an hour while you do nothing. The oven needs fifteen minutes of preheat that the recipe mentions once, at step one, and never accounts for again. That is scheduling, and it renders as a true Gantt: clock time across the top, bars sized by duration, your two hands treated as a resource of capacity one.

Neither structure is derivable from the other. The merge chart has no clock, so it cannot tell you when to preheat. The schedule only knows that four things must precede a fifth, not that those four combine into one substance. That is why views are a registry rather than two hardcoded components: adding one costs a file and a registry entry.

The Scheduler Is the Product

Set a serve time and the plan is computed backward from it. Placement is as-late-as-possible, because food should not sit, which is also why slack renders to the left of a bar: the only room to move is toward the past. Slack itself is measured against a separate forward pass, because under pure backward placement every step sits at its latest start and slack would be zero by construction, making the critical path meaningless.

On the first real test fixture the critical path came out as the oven preheat, not the dough. The dough chain runs 870 seconds; the 900 second preheat has to finish before the final step begins, so it reaches further back. Every dough step carries 210 seconds of slack and the preheat carries none. The binding constraint was the thing the recipe mentions once and never accounts for, which is the entire thesis holding up on the first try.

Both engines are pure functions. The clock is an argument, never Date.now(), which is the single decision that makes a scheduler testable at all.

It Refuses to Guess

A schedule that lies is worse than no schedule, because the chart looks complete and the meal is still late. So a step with no stated time renders as a zero-width marker with a visible “duration unknown” affordance, and it is never filled in. When a source states a total time but no per-step times, the importer says so explicitly and refuses to divide it up.

The same honesty governs import. Structured data only: schema.org JSON-LD, then microdata, then the h-recipe microformat, then stop. There is deliberately no fallback that scrapes body text, because a site publishing no machine-readable recipe has not offered one, and “paste it yourself” is the honest answer. Only the functional recipe is ever stored, never the headnote or the story, and every saved recipe carries its source domain, author and a link back, on screen, in print and in any export.

Some large publishers refuse automated requests at the network edge. Measured against one of them, a mainstream browser User-Agent receives a byte-identical refusal, so this is not about identity and there is nothing to fix in the headers. The app takes them at their word: it says which site said no, offers a paste-the-page path that runs the same parser over a page you opened yourself, and points at a directory of sites measured as working.

What Do I Cook This Week

Scheduling one recipe assumes you already know which recipe. The other half of this app works one step earlier, starting from what’s already in the house rather than a recipe you’ve already picked.

/fridge is a record of what got bought this week, kept after the fact rather than as a shopping list. Each item derives a rough “stretches to how many meals” estimate from a small portions table, shown as a draft and always correctable, and once corrected by hand it stays that way rather than getting quietly re-guessed on the next edit. A week plan then builds from the fridge in the order each item keeps worst first, so nothing bought to spoil sits behind something shelf-stable.

The fridge itself is drawn, above the list you edit it from. A list of eleven rows of number inputs is the wrong density for remembering what’s in the house, so items sit on shelves grouped by category, each a small picture over its name. Categories group the shelves, never how fast something spoils, because labeling a shelf “cook first” would put a claim about food safety right next to the food, which this app refuses to do anywhere. Adding what you bought used to be a hundred-item dropdown; it’s a search box now, and an empty one shows only what’s been starred, since the ingredients bought most weeks are the ones that deserve to be a tap away.

Built for Flour-Covered Hands

Read at arm’s length, on a tablet, by someone who cannot easily touch the screen. Base type is one step larger than a desktop app would use, touch targets are 44px minimum, and numerals are tabular everywhere a time appears, because a clock column whose digits shift width is unreadable at a glance.

Colour is semantic on four orthogonal channels: fill encodes attention, outline encodes the critical path, an end-of-bar glyph encodes status, and hue encodes equipment. Warm means hands-on, cool means walk away, and that mapping never inverts. Resource hues are separated by lightness as well as hue, so the chart survives colour blindness and a greyscale printer. A printed timeline taped to a cupboard is a real use, so both charts print to one page with the attribution in the footer.

A kitchen tablet at night is a real case too, so the app now offers an explicit light or dark theme on top of a system default that still wins with no interaction. Building the override surfaced a bug nobody had ever hit: forcing dark on a browser that reported light used to move nothing but the native scrollbars, because the dark palette had only ever been wired to the system’s own preference. Fixed, with a test that fails if the two palettes needed to support both paths ever drift apart.

Local, and Staying That Way

The library is IndexedDB on the device. No accounts, no sync, no server holding anyone’s recipes. Whole-library export writes a JSON file you keep, and import merges it back, keeping the newer copy of anything that arrives from a second device and never letting an older copy overwrite a local edit.

The app ships with a 300-recipe starter library, built once from Wikibooks Cookbook’s CC BY-SA corpus and committed rather than fetched at runtime. 111 of those recipes carry a photograph: sixty-nine reviewed by hand off a contact sheet of candidates and credited to their photographer, the rest a drawn tint and initial rather than a guessed or borrowed picture, because a wrong photo of the wrong dish is worse than an honest placeholder.

1,049 tests. Two of the more useful ones assert what the app gets wrong on purpose, so the known limits of a best-effort ingredient parser are visible in the suite rather than discovered in a kitchen.

Development timeline

59 logged updates over 35 days, 29 Jul 2026 to 1 Sept 2026.

  1. Our own CSP was blocking our own theme script (complete)feature

    The session close recorded "Cloudflare Web Analytics is blocked by our own CSP" as the open question. Looking at it properly, the framing was wrong twice over.

  2. The phone could not turn the page (complete, deployed )feature

    Each one held on a laptop and quietly evaporated at 390px, which is where this app is used.

  3. The guided tour, and the prose it replacedfeature

    An eleven-step walkthrough that drives the real app across all four Collect sections, opening once on a first visit and re-runnable from a header button.

  4. Deployed: the Plan tab and Explorefeature

    Three things this deploy got right that this vault has previously got wrong. It ran from the directory containing functions/, so the Worker shipped; running from the repo root instead ships a green site with a dead API, which caused an…

  5. Explore: a swipe reader over publisher feeds (complete, NOT deployed)feature

    Suite 1,224 tests across 89 files, tsc --noEmit clean.

  6. Import reaches the Library front doorfeature

    The owner asked for a button at the top of the library to import a recipe, noting that people will want to import from a website and then connect the recipe to a book.

  7. The book that could not gain a recipefeature

    Reported as "there's no easy way to do this in the library or from the specific recipe after I have imported it".

  8. The Plan tab: two columns, a derived shopping list, and a third amendmentfeature

    This is the third amendment to that section: the pantry one on 2026-08-09 turned into the fridge and the week plan, the sharing one on 2026-08-12 opened a narrow provenance-gated link for one book, and this one opens a DERIVED shopping…

Show the earlier 51 entries
  1. Renaming a book you copiedfeature

    Asked for as "can you add a feature to rename the books I've copied", which names the exact set: forkRecipeBook stamps a copy "<source> (my copy)" and nothing in the app could ever change it. There was no rename path at all, for any book.

  2. The release that reached first-time visitors and nobody elsefeature

    Reported as "I don't see it live on recipe.wanessalabs.com", and he was right. The deploy was real, Production, and verified.

  3. The shelf splits in twofeature

    Reported the same hour: the book created by binding is hard to see.

  4. The shipped shelf gets the curator's namefeature

    Asked for directly: the bottom shelf should be named Clemens’ Curated Cookbooks.

  5. Books written as ideas, and a corpus that stops samplingfeature

    The owner asked why "Clemens' Go-To Meals" did not hold the thirty dishes from his own Lists of 30 post. It never did, and nothing had ever been built to make it: the book held FIVE entries, all starter- ids from the Wikibooks corpus,…

  6. The book finally holds the thirty, and an invariant that had been freefeature

    Yesterday's work built the whole machine for writing a book as ideas and binding each one by search, and then did not use it. Clemens' Go-To Meals still held the same five starter recipes it was given on 2026-08-12, chosen by the…

  7. The rest of the gauntlet's round 1, and five of six measured at lastfeature

    The 2026-08-18 gauntlet-loop pilot closed with a four item checklist for the owner. This session works the remaining three.

  8. The reduced-motion override that never won the cascadefeature

    Found by a gauntlet-loop pilot run, not by the test suite. Identical specificity (0,1,0), same origin, both unlayered, and a media query adds no priority, so source order decided and the base value won.

  9. A manual light/dark toggle, and a latent bug it exposedfix

    The owner asked for a light/dark toggle "for the whole site".

  10. The fridge gets steel, an open door, a simpler add flow, and favoritesfeature

    Three separate requests landed in one turn: make the fridge visual look more like an appliance, drop the "Amount" field because most people buy one portion, and replace the ingredient dropdown with search plus starred favorites. All three…

  11. A deriver fix that reached nothing, and the row with several ownersfix

    The duration fix earlier today was correct and invisible. loadStarterLibrary runs derive once and persists the graph, so a library that already existed kept every wrong number.

  12. Crediting the photographer, before there is anything to creditfeature

    Preparation for a decision not yet taken. The Commons probe found 85 candidate photographs matched by DISH NAME for recipes that have none of their own, and those cannot ship without attribution.

  13. Pictures for the 42 that have one, and a drawn stand-in for the 258 that do notfeature

    The library rendered 300 identical chef hats, which reads as a library that failed to load rather than one built from a text cookbook.

  14. The deriver could not read a fraction, and had never been asked tofeature

    The answer is the regex, and the investigation found two more bugs sitting beside it.

  15. The starter library was unreachable to anyone who had saved a recipefeature

    Reported from a real browser: five recipes saved, no shelf books, and no way to get them. The shelf said "The shipped books arrive with the starter library.

  16. Three more occasion books, and moving a ceiling without moving the shelffeature

    Thanksgiving, Christmas and Feed a Crowd. Five books to eight, which is one more than the six cloths the shelf had.

  17. 69 approved photographs, and the release that would have hidden themfeature

    The owner reviewed the 85-card contact sheet and kept 69. Applied with npm run photos:apply, which validated every title against the bundle before writing a byte and resolved every file on Commons with its credit.

  18. The curated-photo lane, and a second lane that turned out to be a dead endfeature

    The 42 recipes whose own Wikibooks page carries a picture are done. This is the path for the other 258, and it is deliberately not the same path.

  19. five books on the shelf, and the curation moves out of TypeScriptfeature

    Phase 2 of the recipe-books rework. Content, planting, and the finalize step.

  20. occasion books, and a merge that reuses the scheduler rather than copying itfeature

    The Library duplicates the feed, and it does so structurally. That is why a Book today is either a publisher or the single staging record, and why neither can express "what do I cook for this situation".

  21. rule 6 opened, narrowly, and a gate that decides per recipefeature

    The amendment came before the code, and rule 6's text was not touched.

  22. the Gantt was not lying about the plan, it was lying about the dayfeature

    A correction to the entry above, and two fixes.

  23. the Library stops being a second copy of the feedfeature

    Phase 3 of the recipe-books rework. The shelf, the book, the create flow, clone and remix, and the export that closes the curation loop.

  24. the book becomes the library, and four feeds that never had a picturefeature

    Three reported problems, one of which turned out to be three problems.

  25. a hundred recipes, and the source that did not survive checkingfeature

    The owner asked to start with roughly a hundred recipes reliably loaded. Getting there by fetching is a hundred fetches, and rule 1 permits one per user action, so the answer was a bundle shipped with the app.

  26. one list across every source, and the rule that stayed putfeature

    Reported by the owner: "it's not easy to see all of the scrapes together. Is there an easier way to just scrape all of the recipes by a name?

  27. the book was unusable on a phone, and a comment said otherwisefeature

    Reported by the owner: on mobile a found book "does not show the left page so it always says this page has not been opened yet". It reproduces on production, and it is worse than the report.

  28. the fridge and the week plan, and a plan that argued with itselffeature

    A stated non-goal was retired on purpose, at the owner's request. prd section 6 banned "no pantry, no shopping, no aggregation across recipes".

  29. the fridge becomes a fridge, and a test that never tested anythingfeature

    /fridge answered "what did I buy" with a column of form rows, each carrying two number inputs and a note about who owned the stretch number. That density is right for editing and wrong for recognition: someone opening the page to remember…

  30. the fridge reaches production, from a tree that was not this onefeature

    --commit-dirty was omitted deliberately, per the recorded gotcha where it silently sends a --branch=main deploy to Preview.

  31. the starter set learns what a weeknight isfeature

    The owner: "I would rather skew towards weeknight." The first hundred were drawn from Category:Recipes, which is everything, and arrived heavy with desserts, condiments and baking. The Cookbook classifies its own pages by course, so the…

  32. looked at it in a browser, and found the feature had never workedfeature

    Everything above this entry was verified by tests and by reading. Opening the app in Chrome against wrangler pages dev, before deploying, found two defects that 631 green tests could not see.

  33. the seam connected, and it was two actions not onefeature

    The previous entry recorded tagging as written down but not wired. Connecting it found that the gap was wider than the tag button: nothing in the app ever moved a found page from stub to fetched.

  34. the shelf and the book, built but not yet reachablefeature

    The library became a shelf. A found book is a publisher, cloth spine, leaning, read only, assembled from that publisher's own feed.

  35. the Gantt learns its container widthfeature

    The chart allocated a flat 4px per minute whatever box it was in. On the 18-minute biscuits in a 1118px viewport that drew a 72px chart, and rendered every one-minute step as a 4px sliver.

  36. the payload measured, and two optimizations declinedfeature

    Two payload items had been carried as open for three sessions. Both were measured properly for the first time, and both turned out to be worth less than they read.

  37. Browse measured and working, deployedfeature

    The blocker was the measurement, and the measurement was takeable. curl to publisher hosts is denied in this environment, which is what stalled phase 5 yesterday.

  38. mobile pass, two-section nav, and two defects caught only in productionfeature

    Two sections declared once. Cook mode renders neither, because /r/:id shares no chrome with Collect and a tab bar over the recipe you are cooking from is the library chrome that route exists to escape.

  39. Redesign phases 1 to 4, and the first time anyone opened the appfeature

    paper, --ink and --tab are canonical and overridden in all three theme blocks; the older names alias them.

  40. first live import, and what it foundfeature

    The owner's first real import returned a 403. URL: allrecipes.com/recipe/21014/good-old-fashioned-pancakes/.

  41. merged, portfolio deployed, and the first live verificationfeature

    The merge ran in a throwaway worktree on main. The shared tree is on another session's branch and carries 60+ uncommitted files, and this vault has a recorded incident of one session's broad git add sweeping another's staged work.

  42. paste-source and discoverfeature

    27 tests added, 285 total green across three consecutive runs.

  43. Phase 0, auditdocs

    Structural template: kalimba-easy. Most recent root project and it ran this same diff-first phase-gated process, including a Phase 0 audit entry in its lineage.

  44. Phase 1, POW scaffolding and specsdocs

    Written in a dedicated git worktree. recipe-flow/ is an entirely new folder that touches nothing existing, so collision is impossible rather than merely unlikely.

  45. Phase 2, domain model and two enginesfeature

    Pure logic. No UI, no network, no Date.now() in either engine.

  46. Phase 3, view registry, three views, editorfeature

    Built in one gate at the owner's instruction, after a proposal to split it into 3a to 3d was declined.

  47. Phase 4, importfeature

    Resumed after the session that started this phase was cut off mid-flight. The seven Phase 4 files existed on disk and had never been run.

  48. Phase 5, libraryfeature

    61 tests added, 255 total green, tsc --noEmit clean, build clean, npm audit reports 0 vulnerabilities. dexie@4.4.4 added, plus fake-indexeddb as a dev dependency.

  49. Phase 6, deploy and wrap-upfeature

    Deployed to Cloudflare Pages Production. Wrangler compiled the Worker, uploaded 3 static files, _headers, and the Functions bundle.

  50. robots.txt caching, and a fail-open bug it exposedfix

    36 tests added, 321 total green across three runs.

  51. the logo, and two sizes it cannot survivefeature

    Owner supplied recipe flow logo.png: a chef-hat lemon over a recipe scroll and a Gantt chart, on an orange band. Added to the app header and to the portfolio page.

Written from this project's decision log as work happened, not afterwards. See every project →

What's next

3 open items

Plans, not promises. Taken from this project's own roadmap, last updated 23 Aug 2026.