Commit graph

129 commits

Author SHA1 Message Date
Jon Seager
4bdfc661f9
refactor: consolidate entity pages into unified /data and /add views
Replace per-entity pages (/roasters, /roasts, /bags, /brews, /gear,
/cafes, /cups) and detail pages with a single tabbed /data view and
a dedicated /add page for entity creation.

- Add /data route with tab-based navigation using Datastar
- Add /add route consolidating all create forms
- Remove per-entity page handlers and standalone templates
- Remove detail page routes, handlers, and templates
- Update ListNavigator to accept String paths for query-param URLs
- Update home page and timeline links to use new /data?type=X paths
- Update nav to reference /data instead of individual entity pages
2026-02-04 20:51:30 +00:00
Jon Seager
50a372015c
feat(backup): expose backup/restore via authenticated API endpoints
- Add GET /api/v1/backup and POST /api/v1/backup/restore endpoints
  behind AuthenticatedUser
- Add BackupService to AppState and BackupClient for HTTP access
- Update CLI backup/restore to use API instead of direct DB access
- Remove --database-url flag from backup and restore commands
- Increase body limit to 50MB for restore endpoint
- Add API and CLI tests for auth, export, restore, and round-trip
- Update README to document auth requirement and API endpoints
2026-02-04 19:38:49 +00:00
Jon Seager
47c89bd725
refactor: simplify overly nested and duplicated code across five modules
- Extract 35-line filter_map closure into parse_cafe() in foursquare.rs
- Unify triple-duplicated search branching in pagination.rs into
  fetch_records() and fetch_count() helpers
- DRY three identical gear-loading blocks in brews.rs via
  load_gear_options() helper
- Flatten nested if/else-if in timeline map_details() using match,
  extract shared is_blank() helper
- Deduplicate JSON decode pattern in backup.rs with decode_json_vec()
  and decode_json_opt() helpers
2026-02-04 19:25:35 +00:00
Jon Seager
939743a224
feat(home): add inline detail subtitles to timeline preview cards
Show contextual details inline with event titles on home page timeline
cards. Each event type picks relevant fields: brew shows roaster and
brewer, roast shows roaster and origin, cafe shows city and country,
cup shows roaster and cafe, others show first three detail values.
2026-02-04 19:14:30 +00:00
Jon Seager
cdf90d1ff3
feat(home): add relative dates to timeline and brew cards
- Add relative_date() helper to views module for humanized timestamps
- Show relative dates on home page timeline preview and brew cards
- Format: "Just now", "Xm ago", "Xh ago", "Yesterday", "Xd/Xw ago"
2026-02-04 19:03:05 +00:00
Jon Seager
91a9900c24
feat(scan): optionally open a bag when scanning coffee
Add "Open a bag of this coffee" checkbox (default: checked) to the
scan form with a configurable amount field (default: 250g). When
enabled, creates a bag and timeline event alongside the roaster/roast.
2026-02-04 17:41:53 +00:00
Jon Seager
f3a4cc7075
feat(home): limit open bags to 3 most recently updated
- Add UpdatedAt variant to BagSortKey
- Sort home page bags by updated_at desc instead of roast_date
- Limit to 3 bags instead of showing all open bags
2026-02-04 17:19:26 +00:00
Jon Seager
d15d58b32c
fix(auth): redirect login and logout to homepage instead of timeline 2026-02-04 17:14:56 +00:00
Jon Seager
e508af4c52
feat(nav): add check-in icon to navigation bar
- Add checkin map-pin icon to shared icons partial
- Add check-in nav link (icon on desktop, icon + text on mobile)
- Only shown when authenticated
- Set nav_active to "checkin" on the check-in page
2026-02-04 17:14:52 +00:00
Jon Seager
ebd2698b42
feat(home): redesign home page with recent brews cards and inline scan
- Replace single "Last Brew" with grid of 3 recent brew cards
- Show brew details with emphasis on weight/volume, grind, and brewer
- Add "Brew Again" action matching bag card button style
- Replace quick-action buttons with always-visible "Scan Bag" section
- Fix activity chip wrapping by widening to w-28 with whitespace-nowrap
- Increase section gap from gap-6 to gap-8 for better visual spacing
2026-02-04 17:14:46 +00:00
Jon Seager
a31c91211c
refactor: require OpenRouter and Foursquare API keys at startup
- Remove has_ai_extract/has_foursquare conditionals from all templates
- Remove boolean fields from template structs and route handlers
- Remove has_ai_extract()/has_foursquare() methods from AppState
- Change API key fields from Option<String> to String in ServerConfig
  and AppState
- Validate keys in run_server() with clear error messages
- Remove runtime key checks from extraction/nearby route handlers
- Update README and CLAUDE.md to reflect required configuration
2026-02-04 16:13:04 +00:00
Jon Seager
706a1fd131
fix(timeline): use consistent "Added" labels and simplify cup title
- Change "Brewed" to "Brew Added" and "Cup" to "Cup Added" for consistency
- Simplify cup timeline title to just the roast name
2026-02-04 16:01:03 +00:00
Jon Seager
fa5386c671
feat(checkin): add roast/cafe summary bars and data attribute selects
- Add roast name and roaster name signals to check-in page
- Show selected cafe city as subtext in cafe summary bar
- Add roast summary bar showing roast name and roaster after selection
- Use data attributes on select options for structured cafe/roast data
- Add name/city fields to CafeOptionView and name/roaster_name to RoastOptionView
- Return roaster name signal from scan endpoint for roast summary display
2026-02-04 16:00:53 +00:00
Jon Seager
e4a304b406
feat(checkin): Datastar-native check-in and nearby cafe search
- Add POST /api/v1/check-in endpoint with CheckInSubmission
- nearby_cafes returns HTML fragment for Datastar, JSON for API
- Add NearbyCafeView and NearbyCafesFragment for server-rendered results
- Rewrite checkin.html: replace checkin.js with Datastar signals,
  @post for submission, @get for nearby search, inline geolocation JS
- Cafe filtering uses data-show with signal-based search
2026-02-04 14:36:55 +00:00
Jon Seager
bc30555447
refactor(extraction): Datastar-native AI extraction across all pages
Extraction endpoints now accept FlexiblePayload (form or JSON) and
return application/json signal patches for Datastar requests. Templates
use data-bind for two-way signal binding and data-on:datastar-fetch
with per-form guards to prevent event bubbling between forms.

- extract_roaster, extract_roast_info, extract_bag_scan return signals
- submit_scan extended with optional image/prompt for combined extract+save
- match_roaster_id fuzzy-matches extracted roaster name to existing IDs
- Templates use data-bind:_signal-name (not data-model) for form binding
- Each datastar-fetch handler guards with its own in-progress signal
2026-02-04 14:36:42 +00:00
Jon Seager
48afde8448
refactor(support): add signal response helpers for Datastar
- render_signals_json() returns application/json signal patches
- render_signals_fragment() returns text/html with data-signals attributes
- kebab_to_camel() converts signal names for JSON responses
- escape_html_attr() for safe attribute embedding
2026-02-04 14:36:28 +00:00
Jon Seager
a3aba1b02a
feat(home): replace scan page with home dashboard and check-in flow
- Add home page at / with scan, last brew, open bags, activity, stats
- Add check-in page at /check-in with cafe search + roast scan + rating
- Use Datastar signals for check-in UI state (steps, rating, selection)
- Bridge async JS (geolocation, fetch) to Datastar via custom events
- Replace nav camera icon with house icon (always visible)
- Redirect /scan to / for backward compatibility
- Delete scan.html, add home.html, checkin.html, checkin.js
2026-02-04 12:55:08 +00:00
Jon Seager
4f7d31d47a
refactor(roasters): remove notes field from Roaster entity
- Add migration 0016 to drop notes column from roasters table
- Remove notes from domain structs, repositories, views, CLI, routes
- Remove notes from AI extraction prompts and ExtractedRoaster struct
- Remove notes from scan page form and submission handler
- Update templates, bootstrap script, backup module, and tests
2026-02-03 21:18:48 +00:00
Jon Seager
ae3c1090de
refactor(cafes,cups): remove notes field from Cafe and Cup entities
- Add migrations 0014 and 0015 to drop notes columns
- Remove notes from domain structs, repositories, views, CLI, routes
- Remove notes textarea/columns from web templates
- Update bootstrap script, tests, backup module, and README
2026-02-03 21:09:34 +00:00
Jon Seager
fb31d0a0c4
refactor(cafes): remove notes field from Cafe entity
- Add migration 0014 to drop the notes column from cafes table
- Remove notes from domain structs, repository, view model, CLI, routes
- Remove notes textarea/column from web templates
- Update bootstrap script and all test files
2026-02-03 21:02:37 +00:00
Jon Seager
5f3507fa92
feat(nearby): add city-based search via Foursquare near param
- Add SearchLocation enum to support coordinates or named location
- Accept optional `near` query param as alternative to lat/lng
- Add city text input with checkbox toggle in cafes template
- Foursquare `near` param enables searching any city worldwide
2026-02-03 20:36:59 +00:00
Jon Seager
ecb010e812
refactor(nearby): replace Nominatim with Foursquare Places API
- Replace osm.rs with foursquare.rs using Foursquare Places Search API
- Add isocountry crate for ISO 3166-1 country code to name conversion
- Override verbose country names (e.g. "United Kingdom" instead of
  "United Kingdom of Great Britain and Northern Ireland")
- Add BREWLOG_FOURSQUARE_API_KEY env var for API authentication
- Update route handler, tests, and template to use Foursquare
- Modernise cafes template JS to ES6+ (const/let, arrow fns, template
  literals)
2026-02-03 20:36:11 +00:00
Jon Seager
3746568dd7
refactor(web): extract shared AI extraction JS into standalone library
Move duplicated photo capture, text extraction, and fetch/UI-toggle
logic from roasters, roasts, and scan templates into a shared
extract.js file served at /extract.js. Each page now provides only
its form-filling callback. Uses ES6+ syntax (const, let, arrow
functions, template literals).
2026-02-03 19:31:00 +00:00
Jon Seager
3e3c57a07f
fix(ai): ensure tasting notes are returned in title case
Update ROAST_PROMPT to explicitly request tasting notes in Title Case
so extracted values like "dark chocolate" come back as "Dark Chocolate".
2026-02-03 19:15:49 +00:00
Jon Seager
84ffe36afc
feat(scan): add bag scanning page with combined AI extraction
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.
2026-02-03 19:15:39 +00:00
Jon Seager
023ad1ac31
refactor(web): add has_ai_extract flag to all page templates
Propagate has_ai_extract boolean through every template struct and
route handler so nav.html can conditionally show the scan link.
2026-02-03 19:14:47 +00:00
Jon Seager
b668d368e7
feat(ai): add AI-powered extraction for roaster and roast forms
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
2026-02-03 18:00:19 +00:00
Jon Seager
a4da0598f6
feat: load environment variables from .env file via dotenvy
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.
2026-02-03 18:00:09 +00:00
Jon Seager
acaadbbcf6
refactor(web): split views.rs into entity-specific submodules
- 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
2026-02-03 16:58:44 +00:00
Jon Seager
714713827d
refactor(routes): extract form option helpers and fragment renderer macro
- 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
2026-02-03 16:49:36 +00:00
Jon Seager
4b55d1b65c
refactor(web): extract map_details to fix too-many-lines clippy warning
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.
2026-02-03 16:39:11 +00:00
Jon Seager
6e2c59814c
feat(cups): add web views, templates, and navigation
- 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
2026-02-03 16:36:35 +00:00
Jon Seager
4e644d5528
feat(cups): add CLI commands for cup management
- Add cup add/list/get/update/delete subcommands
- Wire Cup variant into Commands enum and main dispatch
2026-02-03 16:36:27 +00:00
Jon Seager
a6fad343fc
feat(cups): add route handlers and wire into application state
- 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
2026-02-03 16:36:23 +00:00
Jon Seager
c2e9c7395c
feat(cups): add HTTP client for cups API
- Add CupsClient with create, list, get, update, delete methods
- Register cups() accessor on BrewlogClient
2026-02-03 16:36:14 +00:00
Jon Seager
928c4c9aea
feat(cups): add SQL repository with timeline event integration
- 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
2026-02-03 16:36:10 +00:00
Jon Seager
ac7a4f9cf9
feat(cups): add migration and domain layer for cup entity
- Create cups table with roast_id/cafe_id FKs, optional notes and rating
- Update timeline_events CHECK constraint to include 'cup' entity type
- Add CupId typed wrapper, Cup/CupWithDetails/NewCup/UpdateCup structs
- Add CupFilter, CupSortKey, and CupRepository trait
2026-02-03 16:35:56 +00:00
Jon Seager
5e2ee63a1e
test(cafes): add integration tests for nearby cafe search API
- Add wiremock dev-dependency for HTTP mocking
- Make Nominatim URL configurable via AppState for test injection
- Add spawn_app_with_nominatim_mock() test helper
- Add 6 integration tests covering success, empty results,
  authentication, validation, and upstream failure scenarios
2026-02-03 16:03:54 +00:00
Jon Seager
4cd1203864
feat(cafes): add nearby cafe search via Nominatim API
- 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
2026-02-03 15:56:28 +00:00
Jon Seager
3ab1c50cc8
refactor(timeline): remove map pin icon from timeline title
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.
2026-02-03 15:10:21 +00:00
Jon Seager
3f83d74e20
feat(cafes): add CLI commands, web views, and templates
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.
2026-02-03 14:53:00 +00:00
Jon Seager
1781bff48d
feat(cafes): add route handlers and wire into application state
Add cafe route handlers (CRUD API + web pages with Datastar fragment
support). Register cafe_repo in AppState and mount API and web routes.
2026-02-03 14:52:51 +00:00
Jon Seager
677b77d70d
feat(cafes): add SQL repository, HTTP client, and backup support
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.
2026-02-03 14:52:44 +00:00
Jon Seager
c6ad6a02c7
feat(cafes): add migration and domain layer for cafe entity
Add cafes table with name, slug, city, country, latitude, longitude,
website, and notes fields. Define domain types (Cafe, NewCafe, UpdateCafe),
CafeId typed wrapper, CafeRepository trait, and CafeSortKey enum.
2026-02-03 14:52:37 +00:00
Jon Seager
f1e8984ffa
refactor(roasts): extract helpers to remove too_many_lines suppression
- 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
2026-02-03 14:09:53 +00:00
Jon Seager
09a629db9a
feat(brews): pre-populate Add Brew form with last recorded brew values
- 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
2026-02-03 14:01:41 +00:00
Jon Seager
1fa88d054c
feat(roasts): consolidate table columns and add tasting notes search
- 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
2026-02-03 12:51:59 +00:00
Jon Seager
2edf102b41
feat(search): add search to ListQuery, ListNavigator, and route handlers
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.
2026-02-03 12:51:34 +00:00
Jon Seager
1e151123a0
feat(search): implement search filtering in repository SQL queries
Wire SearchFilter into each repository's list method with entity-specific
searchable columns: roasters (name, country, city), roasts (name, roaster,
origin, producer), bags (roaster, roast), brews (roast, roaster),
gear (make, model). Timeline passes None.
2026-02-03 12:51:26 +00:00
Jon Seager
f616958e3c
feat(search): add search parameter to repository traits
Add `search: Option<&str>` to list methods on RoasterRepository,
RoastRepository, BagRepository, GearRepository, and BrewRepository.
Default trait methods pass None for backwards compatibility.
2026-02-03 12:51:19 +00:00