Commit Graph

107 Commits

Author SHA1 Message Date
jbrechtel d9a7f6538e feat: add Import link to index page navbar
Build and Deploy / build-and-deploy (push) Successful in 1m32s
2026-05-21 10:56:35 -04:00
jbrechtel c36e632d25 style: style import form inputs to match search box
Build and Deploy / build-and-deploy (push) Successful in 1m32s
2026-05-21 10:52:34 -04:00
jbrechtel 5680cc434c Loads config in Docker
Build and Deploy / build-and-deploy (push) Successful in 1m35s
2026-05-21 10:48:52 -04:00
jbrechtel e5e0f1c0e0 chore: add yaml and base64-bytestring dependencies, re-export Config
Build and Deploy / build-and-deploy (push) Successful in 2m1s
2026-05-21 10:34:03 -04:00
jbrechtel 232373cd5c feat: expand import form with file upload via base64 JS 2026-05-21 10:32:49 -04:00
jbrechtel 578d1ae068 feat: add LLM import pipeline with file upload and text input 2026-05-21 10:29:35 -04:00
jbrechtel b179135136 feat: add --config-file CLI flag for YAML config 2026-05-21 10:25:06 -04:00
jbrechtel c015dce51e fix: strip code fences, add MIME/size validation, add tests 2026-05-21 10:23:25 -04:00
jbrechtel 7a3f63e377 feat: add convert-to-cooklang script for Anthropic API recipe conversion 2026-05-21 10:18:23 -04:00
jbrechtel 5765829004 refactor: return Either from loadConfig, add Config tests 2026-05-21 10:16:41 -04:00
jbrechtel 1d5459ea1e feat: add Config module for YAML config loading 2026-05-21 10:10:59 -04:00
jbrechtel 5abf5490b4 feat: add interactive countdown timers to recipe steps
Build and Deploy / build-and-deploy (push) Successful in 1m45s
Cooklang timer references (~{15%minutes}) now render as clickable
spans. Clicking one starts a live countdown displayed in-place
with MM:SS format. A reset button (↺) restarts the timer from
the original duration. A close button (✕) returns to the static
label. When the timer reaches zero, the display pulses and turns
rust-colored for 3 cycles.

Implementation:
- durationToSeconds helper converts Duration to total seconds
- Timer span uses data-seconds attribute for the total
- Label + controls (display, reset, close) structure pre-rendered
  in HTML, toggled via a .running CSS class
- Timer JS initializes all .roux-timer-tag elements on page load
- Each timer manages its own setInterval, multiple timers can
  run simultaneously across different steps
2026-05-20 22:50:35 -04:00
jbrechtel ddb8577c4b feat: add headless Chromium fallback to recipe scraper
Build and Deploy / build-and-deploy (push) Has been cancelled
Uses Playwright to drive Chromium when the primary requests-based
fetch fails (e.g., Cloudflare, JS-required pages). Hybrid approach:
try fast path first, fall back to Chromium only on failure.

Dockerfile: add playwright==1.52.0 pip package, run playwright
install-deps chromium + playwright install chromium to download
the browser and all system libraries.

scrape-recipe: add fetch_with_chromium() that tries Playwright
first, then chromium --headless --dump-dom as a subprocess fallback.
Modify main() to catch primary fetch errors and call Chromium
fallback before giving up.
2026-05-20 22:44:15 -04:00
jbrechtel 8807ba3851 feat: show recipe image alongside description when metadata has one
Build and Deploy / build-and-deploy (push) Successful in 1m30s
Adds a .roux-desc-row flex container that places the recipe
description text on the left and the image (from metaImage)
on the right, using what was previously empty horizontal space.
Image is capped at 280px, with a subtle shadow and rounded
corners. Responsively stacks on mobile.
2026-05-20 22:35:25 -04:00
jbrechtel 1b7f7b61aa fix: tighten typography, fix parser brace leak, polish UI
Build and Deploy / build-and-deploy (push) Successful in 1m40s
- Drop italics on cookware/timer tags — they were over-emphasizing
  roughly a third of step text. Now plain with slight opacity.
- Fix parser bug: @salt{large pinch} braces no longer leak through
  as raw text. Non-numeric quantities are consumed silently,
  ingredient appears without displayed quantity.
- Remove duplicate '\342\206\227 Original' link from marginalia SOURCE block.
  Title-adjacent link is sufficient.
- Add tags to marginalia column for visual balance.
- Constrain content to 1120px max-width for a printed-cookbook feel.
- Add .container h2 CSS specificity override to beat Pico CSS defaults.
  Section labels (INGREDIENTS, METHOD) now properly render in Fraunces.
- Switch navbars from <div> to <nav> so Pico's own nav ul{list-style:none}
  suppresses gray square bullets. Add explicit li{list-style:none} override.
- Constrain content to 1120px max-width so parchment frames the page.
2026-05-20 22:31:23 -04:00
jbrechtel 752918c1af fix: remove extra closing brace in index page JS
Build and Deploy / build-and-deploy (push) Successful in 1m26s
renderByCourse had a duplicate '}' that caused
'SyntaxError: missing ) in parenthetical' on load.
2026-05-20 22:18:17 -04:00
jbrechtel 78764be0a9 refactor(ui): overhaul typography, color palette, and recipe page layout
Build and Deploy / build-and-deploy (push) Successful in 1m35s
Changes based on design feedback:

Typography: Swap Lexend Peta → Fraunces (literary serif) for all
headings, preserving Quicksand for body text.

Color palette: Add --roux-ochre and --roux-sage vars. Restrict rust
accent to step numbers, rules, and hover. Titles and labels → ink.

Navbar: 'Roux' logo now uses Fraunces in ink color.

Parser bug: Add isMethodSection filter to exclude 'Ingredients'
sections from the method column, preventing raw text ingredient
listings from leaking into steps.

Index page: Titles in ink with rust-on-hover. Add letter dividers
(A, B, C...) for alpha mode. Show tags/course metadata under titles.

Recipe page: 3-column grid (240px 1fr 180px) with marginalia column
for source link and notes. Tag pills → sage outline. Ingredient
highlights → ochre underline. Checkboxes → hand-sketched rotation.
Source link → muted ink with rust hover.
2026-05-20 22:13:33 -04:00
jbrechtel bf6fde376a feat: parse schema.org ingredient strings into structured Cooklang ingredients
Build and Deploy / build-and-deploy (push) Successful in 1m45s
Replaces buildIngredientSection's plain-text dump with proper
StepIngredient annotation. Parses ingredient strings like
"3/4 cup extra-virgin olive oil" into:
  - quantity: 3/4
  - unit: "cup"
  - name: "extra-virgin olive oil"

Handles fractions ("3/4"), compound quantities ("1,1/2"),
unit recognition (cup, ounce, pound, etc.), and strips
preparation notes after the first comma.
2026-05-20 18:11:30 -04:00
jbrechtel d29d8b0af0 fix: separate steps with blank lines in Cooklang renderer
Build and Deploy / build-and-deploy (push) Successful in 1m17s
The Cooklang parser (splitByBlankLines) identifies step boundaries by
blank lines (\n\n). renderSection was joining body items with a single
newline (\n), causing multiple steps to be parsed as one. Fixed by using
\n\n as the separator between body items, matching the spec.
2026-05-20 16:04:16 -04:00
jbrechtel c33a51d80b test: add step count preservation tests for SchemaOrg → Cooklang conversion
Build and Deploy / build-and-deploy (push) Successful in 1m14s
Adds checkStepCount helper that verifies the number of instructions in
the parsed schema.org recipe matches the number of SecStep items in
the Method section of the resulting Cooklang Recipe. Tests both
fried-rice (4 steps) and carrot-risotto (7 steps).
2026-05-20 15:57:14 -04:00
jbrechtel 6081534c10 feat: save raw scraper JSON alongside .cook file on import
Build and Deploy / build-and-deploy (push) Successful in 1m18s
When importing a recipe, the raw JSON output from scrape-recipe is now
saved as <recipe-name>.json in the recipe directory alongside the .cook
file. This lets users inspect the intermediate schema.org representation
to debug step breakdown or ingredient parsing issues.
2026-05-20 15:50:17 -04:00
jbrechtel d77f4eb59f fix: persist SSE debounce across page reloads via sessionStorage
Build and Deploy / build-and-deploy (push) Successful in 1m20s
The in-memory lastReload variable was lost on location.reload() because
the JavaScript context is destroyed. This caused a reload loop when the
watcher fired multiple SSE events for the same file change — each reload
reset the debounce to 0, so the next event would trigger another reload.

Switch to sessionStorage, which persists across page loads within the
same browser tab session. Also increases the debounce window to 3000ms
to account for page load time.
2026-05-20 15:42:01 -04:00
jbrechtel 7b7c522080 fix: force lazy ByteString evaluation before waitForProcess to avoid deadlock
Build and Deploy / build-and-deploy (push) Successful in 1m45s
The lazy I/O in LB.hGetContents returns a thunk immediately without
reading any data. If waitForProcess is called before the ByteString
is forced, the parent blocks on the process while the child blocks
on a full pipe buffer (no one is reading). Fix by computing LB.length
(which forces full traversal) before calling waitForProcess.
2026-05-20 15:35:22 -04:00
jbrechtel 7fb83230d3 fix: correct createProcess tuple order in readProcessBytes
Build and Deploy / build-and-deploy (push) Successful in 1m38s
createProcess returns (stdin, stdout, stderr, process), not
(stdout, stderr, _, process). Since stdin is Inherited (not CreatePipe),
it returns Nothing, not Just. The pattern (Just outH, Just errH, _, _)
was matching on the stdin and stdout fields, so it always fell through
to the error branch.
2026-05-20 15:18:11 -04:00
jbrechtel 6f610c5d86 fix: replace partial pattern in readProcessBytes with explicit case
Build and Deploy / build-and-deploy (push) Successful in 1m28s
Also adds type-safety conventions to AGENTS.md: no partial patterns on
IO results, no partial functions (head/tail/fromJust), always use case
with explicit branches.
2026-05-20 15:05:48 -04:00
jbrechtel 8d1ff6eb86 fix: read subprocess output as raw bytes to avoid locale encoding errors
Build and Deploy / build-and-deploy (push) Successful in 1m32s
readProcessWithExitCode returns stdout/stderr as String, which forces GHC
to decode raw bytes using the current locale encoding. In a Docker
container with C locale (default), non-ASCII UTF-8 bytes (like 0xE2)
cause 'invalid argument' errors.

Replace with readProcessBytes which uses createProcess + hSetBinaryMode
to capture stdout/stderr as raw lazy ByteStrings, bypassing locale
decoding entirely. JSON is decoded directly from raw bytes via A.decode.
2026-05-20 14:53:11 -04:00
jbrechtel ebeb52212d fix: properly percent-decode URL-encoded form values in import
Build and Deploy / build-and-deploy (push) Successful in 1m35s
The urldecode helper in parseFormBody was using Html.urlDecode which only
handled %%20, %%23, and %%25. Full URLs encode colons, slashes, and dots
as %%3A, %%2F, and %%2E, so they were never decoded. The scraper received
the still-encoded URL and failed to fetch it.

Replace Html.urlDecode with a proper percent-decode that handles all %%XX
hex sequences using digitToInt.
2026-05-20 14:46:43 -04:00
jbrechtel 72535fb3ec fix: disable stdout/stderr buffering for Docker log visibility
Build and Deploy / build-and-deploy (push) Successful in 1m23s
2026-05-20 14:38:52 -04:00
jbrechtel 98644be7ea feat: add structured logging to import pipeline
Build and Deploy / build-and-deploy (push) Successful in 1m17s
Logs each stage of the import with [roux] prefix: request URL, script
path, exit codes, stderr output, JSON parse status, conversion status,
and file write path. Error messages now include the attempted URL for
easier debugging.
2026-05-20 14:33:08 -04:00
jbrechtel 29952d456c fix: resolve scrape-recipe script path via PATH then fallback to scripts/
Build and Deploy / build-and-deploy (push) Successful in 1m20s
2026-05-20 14:25:37 -04:00
jbrechtel 81723622be chore: re-export CooklangPrint from Roux module
Build and Deploy / build-and-deploy (push) Successful in 1m57s
2026-05-20 13:23:12 -04:00
jbrechtel 6e895f7286 feat: add /import route for recipe URL import 2026-05-20 13:22:27 -04:00
jbrechtel 4738972046 feat: add import form and result page templates 2026-05-20 13:07:31 -04:00
jbrechtel e1c2a4bdc5 feat: add CooklangPrint module for rendering Recipe to .cook text 2026-05-20 13:06:23 -04:00
jbrechtel 751a9f853c docs: add recipe import design spec
Build and Deploy / build-and-deploy (push) Successful in 1m17s
2026-05-20 12:55:21 -04:00
jbrechtel 602324a699 chore: use persistent Docker volume for stack-root instead of host directory
Build and Deploy / build-and-deploy (push) Successful in 1m36s
This allows all git worktrees to share a single cached GHC and
dependency store, avoiding recompilation per worktree.
2026-05-20 12:09:03 -04:00
jbrechtel 2628fcf6d9 live update docs
Build and Deploy / build-and-deploy (push) Successful in 1m22s
2026-05-20 11:57:58 -04:00
jbrechtel 795a99ae2b fix: deduplicate SSE events and debounce client reload
Build and Deploy / build-and-deploy (push) Successful in 1m39s
2026-05-20 11:54:06 -04:00
jbrechtel 71cef21daf feat: add SSE client JS for live recipe reload on detail and index pages
Build and Deploy / build-and-deploy (push) Successful in 1m39s
2026-05-20 10:26:50 -04:00
jbrechtel f5c693a2b5 feat: add SSE endpoint and ChangeLog for live recipe reload 2026-05-20 10:21:57 -04:00
jbrechtel 7a6d3653a7 Merge branch 'live' into main — live recipe watching via fsnotify
Build and Deploy / build-and-deploy (push) Successful in 4m30s
2026-05-20 06:30:07 -04:00
jbrechtel ce9062a4b3 fix: catch IOExceptions in reReadRecipe and wrap watchDir callback for resilience 2026-05-19 23:40:13 -04:00
jbrechtel 956a4bc7d7 fix: use watchDir callback instead of watchTreeChan for reliable event delivery 2026-05-19 23:35:32 -04:00
jbrechtel a7434eced7 Merge branch 'utf8'
Build and Deploy / build-and-deploy (push) Successful in 1m43s
2026-05-19 23:29:45 -04:00
jbrechtel 5bfd3f2308 fix: use absolute dir path for watcher makeRelative consistency 2026-05-19 23:28:51 -04:00
jbrechtel 166a2e7800 Handles UTF8 recipe files 2026-05-19 23:28:37 -04:00
jbrechtel 8f8dbecea5 refactor: replace aeson ToJSON/FromJSON with json-fleece schemas
- RecipeSearchEntry: replace genericToJSON ToJSON with Fleece schema
  using object combinators (required, optionalNullable, list text)
- SchemaOrgRecipe: replace manual FromJSON instance with Fleece decoder
  that reuses existing helper functions for quirky fields
- Html.hs: use Fleece.Aeson.Encoder.encode instead of Data.Aeson.encode
- stack.yaml: add shrubbery + json-fleece extra-deps
- package.yaml: add json-fleece-aeson + json-fleece-core deps
- Remove unused LANGUAGE pragmas and redundant brackets
2026-05-19 23:24:24 -04:00
jbrechtel d8e2cc101d fix: correct path handling in live recipe watching
- Strip .cook from map keys to match lookupRecipe behavior
- Use makeRelative for event paths to match relative map keys
- Filter watcher to only .cook files
- Log watcher thread exceptions
- Use imported isSuffixOf from Data.List
2026-05-19 23:23:04 -04:00
jbrechtel a2a8ea7b99 feat: watch recipe directory for live updates via fsnotify 2026-05-19 23:14:24 -04:00
jbrechtel a8402cf831 chore: add fsnotify dependency for live recipe watching 2026-05-19 23:08:02 -04:00