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.
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.
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.
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.
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.
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.
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.
- 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
- scripts/scrape-recipe: Python script using recipe-scrapers to extract
schema.org JSON-LD from a recipe URL (or local HTML file for testing)
- Dockerfile: add Python3 + recipe-scrapers in virtualenv, install the
scrape-recipe script at /usr/local/bin/scrape-recipe
- Dockerfile: production image for roux-server (Debian slim, tini, binary)
- docker-compose.yml: production compose with recipe/data volume mounts
- .gitea/workflows/deploy.yaml: Gitea Actions pipeline that builds, tests,
pushes Docker image, and deploys via SSH
Follows the same pattern used by the Atlas project.
Haskell\'s \' within double-quoted string literals is interpreted as
just \' (no-op escape). Switched to \\' to produce \' in the JS
output, which correctly escapes the single quotes inside the
single-quoted JS string literal for the CSS class name.
Installs a pre-commit hook that checks all staged .hs files are
formatted by fourmolu before allowing a commit. Configured via
core.hooksPath = .githooks.
Run: ./hs fourmolu -i <files> to format
- Rewrites indexPage to render a shell page with embedded JSON + JS
- Adds searchJs with vanilla JS for client-side list rendering, filtering, sorting
- Hash-based sort modes (#alpha, #tags, #course)
- Search filters by title and filename (case-insensitive)
- Fix: recipe links use /recipes/ prefix and encodeURIComponent for # encoding
- Fix: avoid double-render on initial page load