When the trigger JS was updated to use querySelectorAll('.roux-record-trigger')
for dual-button support, the variable was renamed from 'btn' to 'btns' but the
submit handler still referenced 'btn'. This commit:
- Adds activeBtn tracking: the button that opened the modal is stored in
activeBtn via 'this' in the click handler
- Uses activeBtn.dataset.filename in the submit handler (with null guard)
- Removes the stale btn reference
- Card images now show the recipe's actual image when available
(background-image: url(...) with background-size: contain to preserve
aspect ratio without distortion), falling back to gradient placeholders
- Cards are now wrapped in <a href="/recipes/{filename}"> so clicking
anywhere on a card navigates to the recipe page
- Added .contain CSS class for image-fit behavior and .lnd-card a for
link styling
- New landing page (/) based on roux-landing-mockup.html layout
- Shuffle card that suggests random recipes
- Recently cooked shelf (driven by cook log DB)
- Recently added shelf (by file modification time)
- Existing recipe index moved to /recipes
- Updated nav links throughout to point to /recipes
- All hlint warnings fixed
- Use newtype instead of data for single-field types (RouxConfig, ImportError)
- Replace maybe "" id with fromMaybe "" throughout
- Replace not (x `elem` ys) with x `notElem` ys
- Remove redundant brackets, redundant $, and eta-reduce lookupRecipe
- Use numeric underscore for 1_000_000
Previously images were saved directly in the recipe directory
alongside .cook files. Now they go into <recipes-dir>/recipe-images/,
keeping the recipe directory clean and making the purpose of the
file clear from its path.
Users can now upload an photo for any recipe directly from the
view page. The upload button appears as either a 'Replace' overlay
on existing images or an empty dashed placeholder for recipes
without one.
Upload flow:
1. Click upload button -> file picker opens (accepts image/*)
2. JavaScript reads the file as base64, POSTs to /upload-image
with form fields: filename, file-b64, file-name
3. Server decodes the base64, saves to <recipe-dir>/<basename>.<ext>
4. Server updates the .cook file's YAML front matter, adding or
updating the 'image:' key with a /recipe-images/ URL
5. Server returns JSON success, page reloads to show the image
Server routes added:
- POST /upload-image — accepts base64-encoded image upload
- GET /recipe-images/<filename> — serves saved recipe images
(extension-whitelisted to .jpg/.jpeg/.png/.gif/.webp)
Image files are stored alongside .cook files in the recipe directory
and served via the /recipe-images/ path. The fsnotify watcher detects
the .cook metadata change and triggers an SSE reload.
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