Hide input controls and display a spinner with "Waiting for
response..." while LLM extraction requests are in-flight. Controls
are restored on completion or failure. Applied consistently across
scan, roaster, and roast forms.
Add /scan page that lets authenticated users photograph or describe a
coffee bag, extracts both roaster and roast data via a single AI call,
and creates both entities on submit. Existing roasters are matched by
slug to avoid duplicates. Camera icon added to nav bar.
Users can upload a photo of a coffee bag or type a text prompt to
auto-fill roaster/roast form fields via OpenRouter's chat completions
API. The feature is opt-in: hidden when BREWLOG_OPENROUTER_API_KEY is
not set.
- Add src/infrastructure/ai.rs with OpenRouter client, prompt
templates, JSON extraction from model responses, and unit tests
- Add extract-roaster and extract-roast API endpoints behind auth
- Add extraction UI (photo button + text input) to both form templates
- Add openrouter_api_key and openrouter_model to CLI args and AppState
- Enable reqwest gzip feature for compressed API responses
- Default model set to openrouter/free (auto-selects available models)
- Prompts instruct model to use web search for missing details
Add dotenvy::dotenv() call before CLI argument parsing so that
environment variables (e.g. API keys) can be set in a .env file
instead of the shell environment. Add .env to .gitignore.
- Move entity view models into views/{entity}.rs submodules
- Keep Paginated and ListNavigator in views/mod.rs as shared infrastructure
- Re-export all types from mod.rs to preserve existing import paths
- Add load_roaster_options, load_roast_options, load_cafe_options
helpers to support.rs, replacing duplicated inline loading in
roasts.rs, bags.rs, and cups.rs
- Add define_list_fragment_renderer! macro for the 5 structurally
identical render_*_list_fragment functions
- Fix gear.rs to return navigator from load_gear_page like all other
loaders instead of discarding and reconstructing it
Extract the detail-mapping loop from TimelineEventView::from_domain
into a private map_details helper method, bringing the function under
clippy's 100-line limit.
- Pair roasts with cafes visited, with tasting notes and ratings
- Timestamps align with cafe visit dates across the 6-month spread
- Include cup timeline event timestamp distribution
- Test CRUD operations: create, list, get, update, delete
- Test auth requirement, optional fields, enriched data
- Test validation: no changes returns 400, nonexistent returns 404
- Update test helpers with cup_repo wiring
- Add CupView, CafeOptionView, RoastOptionView view models
- Add CupsTemplate (full page) and CupListTemplate (fragment)
- Create cups.html with form and cup_list.html with responsive table
- Add Cups link to desktop and mobile navigation
- Add cup timeline event link and label support
- Add cup_repo to AppState and server startup
- Register API routes: GET/POST /cups, GET/PUT/DELETE /cups/:id
- Register web route: GET /cups with Datastar fragment support
- Rating validation (1-5) on create and update
- Implement CupRepository trait for SqlCupRepository
- Transactional insert creates cup and timeline event atomically
- Support filtering by cafe_id/roast_id, search across roast/roaster/cafe names
- Use QueryBuilder with push_update_field! macro for updates
- Add "Find nearby" button that requests browser geolocation
- Show search input with debounced type-ahead after locating user
- Render clickable results with name, location, and distance
- Auto-fill form fields (name, city, country, lat, lng, website)
when a result is selected
- Show loading spinner in search input during API requests
- Add OSM infrastructure module with Nominatim forward search, viewbox
location bias, and haversine distance calculation
- Add reqwest::Client to AppState for server-side HTTP requests
- Add GET /api/v1/nearby-cafes endpoint (authenticated) accepting
lat, lng, and text query parameters
- Check website, contact:website, url, contact:url, and brand:website
tags to maximise website coverage from OSM data
Position is already shown as a clickable detail row in the card body,
making the title icon redundant. Also removes the unused map_link field
from TimelineEventView.
Add cafe entries distributed over the last 6 months with timestamp
updates for both cafes and timeline events. Add .timeout to sqlite3
heredoc to handle database lock contention.
Add 14 server API tests, 6 CLI tests, 4 Datastar fragment tests,
1 timeline event test, and backup round-trip verification. Wire
cafe_repo into test helpers.
Add CLI subcommands (add, list, get, update, delete) with negative
number support for coordinates. Add CafeView with map URL generation,
Askama templates for list/detail pages, and nav link between Brews
and Gear.
Implement SqlCafeRepository with CRUD operations and timeline event
creation. Add CafesClient for CLI HTTP access. Wire cafes into backup
export/restore and empty-database verification.
- Add empty_to_none() to replace four repeated empty-string-to-None blocks
- Extract insert_timeline_event() from insert() to isolate timeline logic
- Remove #[allow(clippy::too_many_lines)] from RoastRepository impl
- Add BrewDefaultsView to carry last brew's values to the template
- Extract form data loading into load_brew_form_data() helper
- Use defaults for signal initialization and dropdown pre-selection
- Retain submitted values after form submission instead of resetting
- Fix stale fragment rendering example (into_request → into_request_and_search)
- Correct domain conversion convention to to_domain() and fold into Repository Pattern
- Add listing.rs and missing ID types to architecture tree
- Document define_enriched_get_handler! macro
- Add Route Module Structure pattern (load/page/fragment trio, build_page_view)
- Note submission types vs direct domain structs in FlexiblePayload
Datastar sends all non-underscore signals with every backend request.
UI-only signals (showForm, navOpen, isSubmitting, form ref, brew
parameters) were being serialized into pagination/search/sort URLs,
creating bloated requests. Prefix them with _ so Datastar's default
filterSignals excludes them from requests.
Remove wrapper div around month headings so the h2 is a direct child
of the timeline container. position: sticky was broken because the
heading's containing block (the wrapper) was only as tall as the
heading itself, giving it no room to stick.
Also update MutationObserver to detect headings added as direct
children via node.matches() in addition to node.querySelector().
- Move timeline line to centre on mobile instead of left gutter
- Remove left padding from cards to maximise horizontal space
- Reposition dots to top-centre of each card border on mobile
- Make card background fully opaque so line doesn't show through
- Desktop alternating layout unchanged
- Add width: 100% and min-width: 0 to .input-field so selects/inputs
fill their container instead of expanding to fit content
- Add overflow-hidden to the brews form as a safety net
Wrap the brew-again form and delete button in an inline-flex container
to match the horizontal layout used in other tables. Fold water
temperature into the Recipe column as desktop subtext alongside the
ratio, reducing the table from 7 to 6 columns and eliminating the
horizontal scrollbar. Document the Actions column pattern in CLAUDE.md.
Move the Added/Date column to first position in brew, gear, and bag
list templates and apply consistent text-xs font-medium text-stone-600
styling. Document the pattern in CLAUDE.md.
The sentinel div (h-4) added extra space below the pagination bar
on pages with a next page, making the pagination bar appear taller
than on the last page. Since infinite scroll only runs on mobile,
hide the sentinel on desktop with md:hidden.
- Combine Country/City into Location column with city as desktop subtext
- Add City as separate mobile-only field for card layout
- Move homepage icon into Actions column alongside delete button
- Remove bordered style from homepage icon to match other action icons
- Change empty state condition to also check navigator.has_search()
- Show "No X match your search" message inside the table container
- Applied to roast, roaster, and gear list templates
- Combine Roast/Roaster into single column with roaster as subtext on desktop
- Combine Origin/Producer into single column with producer as subtext on desktop
- Add Roaster and Producer as separate mobile-only fields for card layout
- Replace tasting notes pill badges with comma-separated list
- Rename "Notes" column to "Tasting Notes" with right-aligned mobile text
- Include tasting_notes in repository search filter columns
Add IntersectionObserver-based infinite scroll for mobile viewports
that fetches next pages as the user scrolls. Uses MutationObserver
to re-attach after DOM replacements. Hide pagination controls on
screens narrower than 768px.
Add search_header macro with Datastar-driven live search and debounced
input. Move pagination controls from above to below tables. Add
pagination-controls class for mobile hiding. Add infinite scroll
sentinel divs and data attributes to all list partials.
Add q parameter to ListQuery with into_request_and_search(). Extend
ListNavigator with search state, search_value(), has_search(), path(),
and search_query_base() for template URL generation. Thread search
through all route handlers and update backup tests.
Add `search: Option<&str>` to list methods on RoasterRepository,
RoastRepository, BagRepository, GearRepository, and BrewRepository.
Default trait methods pass None for backwards compatibility.
Add SearchFilter struct for LIKE-based search with dynamic column
matching. Add DatabaseDriver type alias to support QueryBuilder
generics across SQLite and Postgres feature flags.