Commit graph

188 commits

Author SHA1 Message Date
Jon Seager
9eb9a9a56c
feat(account): display OpenRouter usage on account page
Show cumulative cost, API call count, and total tokens in card
layout when the user has AI usage data.
2026-02-05 12:09:43 +00:00
Jon Seager
0a93df845f
feat(ai): record usage after each OpenRouter extraction
Wire AiUsageRepository into AppState and add a fire-and-forget
record_ai_usage helper. All extraction route handlers now capture
the usage tuple and record it in the background.
2026-02-05 12:09:36 +00:00
Jon Seager
6271b0e5e5
feat(ai): capture usage data from OpenRouter API responses
Add Usage struct to parse prompt_tokens, completion_tokens,
total_tokens, and cost from every OpenRouter response. Extraction
functions now return (result, Option<Usage>) tuples. New ai_usage
table stores per-call records with a repository trait and SQL
implementation.
2026-02-05 12:08:48 +00:00
Jon Seager
6a0d94739b
feat(auth): allow naming the initial passkey during registration
- Add "Passkey Name" field to the registration form
- Pass passkey_name through start/finish WebAuthn ceremony
- Use user-provided name instead of hardcoded "default"
2026-02-05 11:24:52 +00:00
Jon Seager
16a4704fcf
feat(account): add account page with passkey and token management
- Add /account page with passkey list, token list, and sign-out
- Replace nav login/logout buttons with user icon linking to /account or /login
- Add passkey management: add new passkeys (WebAuthn ceremony), delete with confirmation
- Add token management: create, copy one-time value, revoke with confirmation
- Add user/key/clipboard icon macros
- Add PasskeyCredentialRepository::get() for ownership verification
- Add WebAuthn passkey/start and passkey/finish endpoints for adding passkeys
2026-02-05 11:24:43 +00:00
Jon Seager
03e03d87d9
feat(auth): replace password auth with WebAuthn passkeys
Replace username/password authentication with FIDO2/WebAuthn passkey-based
auth using webauthn-rs. Sessions and bearer tokens are unchanged — only the
way they are created changes.

- Add webauthn-rs, uuid, open, url deps; remove argon2, rpassword
- Add passkey_credentials and registration_tokens tables (migrations 17-18)
- Add domain entities, typed IDs, and repository traits for passkeys/tokens
- Add SQL repository implementations for passkeys and registration tokens
- Add ChallengeStore for in-memory WebAuthn ceremony state
- Add WebAuthn route handlers (register/auth start+finish ceremonies)
- Add CLI browser handoff for token creation (opens browser, local callback)
- Replace login form with "Sign in with Passkey" button
- Add registration page for first-user bootstrap via one-time token
- Replace BREWLOG_ADMIN_USERNAME/PASSWORD with BREWLOG_RP_ID/RP_ORIGIN
- Change default BREWLOG_URL from 127.0.0.1 to localhost (WebAuthn requires it)
2026-02-05 11:00:07 +00:00
Jon Seager
8c521f0e05
feat(data): redesign mobile data page with card layout and tab selector
- Add collapsible mobile tab selector with chevron icons
- Integrate search bar into tab component (below tabs)
- Replace Datastar search handler with vanilla JS for reliability
- Add mobile card layout: card-title, card-date, card-actions classes
- Add three-dot action menu for mobile with details/summary pattern
- Hide duplicate search inside data-content via CSS
- Pass search_value to DataTemplate for input preservation
2026-02-04 22:22:18 +00:00
Jon Seager
dd08e84572
fix(data): fix search bar 400 error on data page
Split Query<DataQuery> (which used #[serde(flatten)]) into two
separate extractors, since serde_urlencoded doesn't support flatten
and coerces all flattened values to strings, breaking u32 parsing.
2026-02-04 20:57:26 +00:00
Jon Seager
2eca0ae3c6
refactor(bags): replace dual-section bag list with single paginated table
Remove the open-bags card grid and closed-bags history table in favour
of a unified table showing all bags with status badges and actions.

- Simplify load_bag_page to single BagFilter::all() query
- Remove open_bags field from BagPageData and BagListTemplate
- Add Status column with Open/Closed badges and remaining weight
- Add Close and Delete action buttons per row
2026-02-04 20:53:12 +00:00
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
Jon Seager
18d39d13ef
feat(search): add SearchFilter and DatabaseDriver to pagination infrastructure
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.
2026-02-03 12:51:08 +00:00
Jon Seager
2ac61345ec
feat(backup): add CLI backup and restore commands
- Add BackupData struct and BackupService with raw SQL export/import
- Restore uses raw inserts to bypass brew deductions and timeline creation
- Restore requires an empty database, inserts in FK dependency order
- Add comprehensive e2e test verifying full round-trip fidelity
2026-02-03 12:50:41 +00:00
Jon Seager
8b77eb90eb
feat(brews): add filter paper to routes, views, CLI, and templates
- Add filter paper dropdown to brew form and gear category form
- Add custom deserializer for optional GearId from HTML forms
- Load filter papers in brews_page handler
- Show filter paper as subtext under brewer in brew list
- Add filter paper to timeline event details and brew-again forms
- Add --filter-paper-id flag to CLI brew add command
2026-02-03 10:14:22 +00:00
Jon Seager
d636f6c246
feat(brews): support filter paper in repository and HTTP client
- Add filter_paper_id to SQL queries (SELECT, INSERT, LEFT JOIN)
- Add filter_paper_name via LEFT JOIN on gear table
- Update BrewRecord and BrewWithDetailsRecord structs
- Add filter_paper_id parameter to HTTP client create method
2026-02-03 10:14:00 +00:00
Jon Seager
0b967cbb14
feat(gear): add filter paper category and optional brew association
- Add migration to extend gear CHECK constraint with 'filter_paper'
  and add nullable filter_paper_id column to brews
- Add FilterPaper variant to GearCategory enum
- Add filter_paper_id to Brew, NewBrew, and BrewWithDetails
- Add filter_paper_id to TimelineBrewData
2026-02-03 10:13:35 +00:00
Jon Seager
80849f2bf5
refactor(brews): extract brew timeline event builder into helper
Move timeline event construction out of create_brew to keep the
handler under clippy's too_many_lines limit.
2026-02-03 08:56:29 +00:00
Jon Seager
949f65ac16
refactor(timeline): denormalize data and simplify frontend
Database:
- Add slug, roaster_slug, brew_data_json columns to timeline_events
- Migration backfills existing data from related tables
- Remove 9-way LEFT JOIN from list query, read directly from columns

Frontend:
- Use CSS :nth-of-type(odd/even) for alternating timeline layout
- Remove JavaScript class manipulation when appending month events
- Simplify infinite scroll month-merging logic

This eliminates query-time JOINs across 5 tables and ~20 lines of
client-side JavaScript for pattern maintenance.
2026-02-03 08:53:29 +00:00
Jon Seager
267ef2bf17
feat(timeline): remove sidebar nav, include sticky headers on scroll
- Simplify timeline layout to single-column without sidebar
- Update tests to explicitly pass page_size for pagination testing
- Include sticky headers and a neater alternating, side-by-side
  timeline design
2026-02-02 21:18:20 +00:00
Jon Seager
b12ad2bf6d
feat(timeline): add "brew again" button to timeline
- Add TimelineBrewData struct to domain layer to carry raw brew
  parameters needed for repeating a brew
- Extend timeline SQL query to LEFT JOIN with brews table and fetch
  brew data (bag_id, grinder_id, brewer_id, coffee_weight, etc.)
- Add brew_data field to TimelineEventView for template access
- Add is_authenticated to TimelineChunkTemplate for auth-aware chunks
- Add "brew again" button to timeline month partial, visible only for
  authenticated users on brew events
- Server returns reload script when brew is created from timeline page,
  using Datastar's fragment patching to append script to body
2026-02-02 20:14:51 +00:00
Jon Seager
9402d98bb6
fix(brews): improve table layout and add "brew again" feature
- Combine weight/water/ratio into single "Recipe" column
- Reorder columns: Coffee, Grind, Recipe, Temp, Brewer, Date, Actions
- Style actions column with icon buttons (consistent with roasts)
- Add "brew again" button to repeat a brew with same parameters
- Use data-model for form inputs (fixes Datastar binding error)
- Add raw values to BrewView for repeat brew functionality
2026-02-02 19:55:18 +00:00
Jon Seager
c4357f2698
feat(brews): add web views and templates
- Add BrewView, BagOptionView, GearOptionView
- Add brews page with form and +/- adjustment buttons
- Add brew list partial with sortable columns
- Add Brews link to navigation
2026-02-02 19:31:46 +00:00
Jon Seager
35d64eaed2
feat(brews): add CLI commands
- Add brew add/list/get/delete subcommands
- Defaults: 15g coffee, 6.0 grind, 250ml water, 91°C temp
2026-02-02 19:31:14 +00:00
Jon Seager
6b7b7f9c6e
feat(brews): add application routes and server integration
- Add brew routes: page, create, list, get, delete
- Add timeline event creation when brews are logged
- Add brew_repo to AppState
- Add Conflict variant to AppError for 409 responses
2026-02-02 19:30:45 +00:00
Jon Seager
5f8de35404
feat(brews): add infrastructure layer
- Add SqlBrewRepository with transactional insert that deducts from bag
- Return Conflict error when insufficient coffee in bag
- Add BrewsClient for CLI HTTP operations
2026-02-02 19:30:14 +00:00
Jon Seager
91bd3172ea
feat(brews): add domain layer and database migration
- Add BrewId typed wrapper
- Add Brew, BrewWithDetails, NewBrew, BrewFilter, BrewSortKey
- Add BrewRepository trait with transactional insert semantics
- Add brews table with foreign keys to bags and gear
- Update timeline_events constraint to include 'brew' entity type
2026-02-02 19:29:23 +00:00
Jon Seager
2bd008a8d7
chore(lint): enable clippy pedantic and restriction lints
- Add [lints.clippy] section to Cargo.toml with pedantic group
- Cherry-pick restriction lints: dbg_macro (deny), todo, unwrap_used, expect_used (warn)
- Allow noisy pedantic lints (missing_errors_doc, module_name_repetitions, etc.)
- Fix unnecessary Result wrappers in token and user repository to_domain functions
- Merge duplicate match arms in TimelineEventViewModel
- Add justified #[allow] attributes for startup code and tests
2026-02-02 17:55:16 +00:00
Jon Seager
00477bdbc0
feat(roast): add update command for consistency with other entities
- Add PUT /api/v1/roasts/:id route handler
- Add update() method to roasts HTTP client
- Add UpdateRoastCommand to CLI with optional fields
- Add CLI tests for roast update authentication and functionality

Brings roast entity in line with roaster, bag, and gear which all
support add/list/get/update/delete operations.
2026-02-02 17:09:32 +00:00
Jon Seager
0090c4ba43
refactor(cli): restructure commands from flat to nested subcommands
Change CLI structure from `brewlog {verb}-{entity}` to `brewlog {entity} {verb}`:
- brewlog add-roaster → brewlog roaster add
- brewlog list-bags → brewlog bag list
- brewlog create-token → brewlog token create
- etc.

Each entity module now owns its subcommand enum and dispatch logic,
simplifying main.rs and improving discoverability via `brewlog {entity} --help`.

- Add RoasterCommands, RoastCommands, BagCommands, GearCommands, TokenCommands enums
- Add run() dispatcher to each entity module
- Simplify top-level Commands enum to delegate to entity modules
- Update all CLI tests and bootstrap script
2026-02-02 17:02:46 +00:00
Jon Seager
b3c13faacf
fix(api): make all roast and bag endpoints return enriched types
Previously, list endpoints returned enriched types (RoastWithRoaster,
BagWithRoast) with related entity names, while get/create/update
endpoints returned bare types without this information.

This change makes all endpoints consistent by returning enriched types:
- Added get_with_roaster and get_with_roast repository methods
- Created define_enriched_get_handler! macro for custom getter methods
- Updated create and update handlers to fetch enriched data after write
- Updated CLI client to expect enriched types
2026-02-02 16:43:31 +00:00
Jon Seager
bee94d2a2e
fix(api): flatten RoastWithRoaster JSON response for consistency
The list-roasts API was returning nested objects with a "roast" field,
which was inconsistent with other list APIs in the codebase. Added
serde's #[serde(flatten)] attribute to make the response flat while
maintaining the internal struct composition.
2026-02-02 16:32:56 +00:00
Jon Seager
6bbbafbef4
refactor(gear): remove notes field from Gear entity
The notes field was not providing enough value to justify its presence.
Simplified the Gear entity by removing notes from:
- Domain structs (Gear, NewGear, UpdateGear)
- SQL repository queries and GearRecord
- HTTP client methods
- CLI commands (--notes flag)
- Web views and templates
- All related tests

Added migration 0008_remove_gear_notes.sql to drop the column.
2026-02-02 16:16:54 +00:00
Jon Seager
708d89d452
fix: resolve clippy warnings for argument count and FromStr trait
- Allow too_many_arguments for AppState::new since 8 repos are needed
- Implement FromStr trait for GearCategory instead of custom from_str
  method to follow Rust conventions
- Update callers to use map_err for Result handling
2026-02-02 16:12:13 +00:00
Jon Seager
d1ebe835f9
fix: ensure gear events show up in timeline 2026-02-02 15:58:42 +00:00
Jon Seager
74a8bef475
feat(web): add Gear web UI with reactive updates
Implement complete web interface for the Gear entity:
- Add GearView model with category badges and formatted display
- Create GearTemplate and GearListTemplate for Askama rendering
- Build main gear page with collapsible add form (Datastar-powered)
- Implement gear list table with sortable columns and pagination
- Use trash icon for delete actions matching roasts table design
- Add Gear navigation link in main menu between Bags and Timeline
- Integrate gear events into timeline view with proper labels and links

The web UI follows the established patterns from other entities with
Datastar for reactive fragment updates and proper authentication gating.

Apply code formatting fixes across all gear-related modules.
2026-02-02 15:35:57 +00:00
Jon Seager
1f5561d7ab
feat(cli): add Gear CLI commands
Implement complete CLI interface for managing gear via brewlog CLI.

Commands (presentation/cli/gear.rs):
- add-gear: Create gear with --category, --make, --model, --notes flags
- list-gear: List all gear with optional --category filter
- get-gear: Retrieve single gear by --id
- update-gear: Update gear fields (make, model, notes)
- delete-gear: Delete gear by --id

Uses define_get_command! and define_delete_command! macros for get/delete
operations to reduce boilerplate.

Command Registration:
- Register gear module in presentation/cli/mod.rs
- Add commands to Commands enum: AddGear, ListGear, GetGear, UpdateGear, DeleteGear
- Wire command execution in main.rs

All commands output JSON and use BrewlogClient for HTTP API calls.

Examples:
  brewlog add-gear --category grinder --make Baratza --model Encore
  brewlog list-gear --category brewer
  brewlog update-gear --id 1 --notes "Updated notes"
2026-02-02 15:24:34 +00:00
Jon Seager
1ef447157e
feat(application): add Gear API routes and web handlers
Implement complete HTTP interface for Gear entity with API endpoints and web views.

Route Handlers (application/routes/gear.rs):
- gear_page(): Web page handler with Datastar fragment support
- create_gear(): Create gear with timeline event logging
- list_gear(): JSON API with optional category filter
- get_gear(): Single gear retrieval (using macro)
- update_gear(): Update gear fields
- delete_gear(): Delete gear (using macro)
- load_gear_page(): Helper for paginated gear data
- render_gear_list_fragment(): Fragment rendering for Datastar updates
- NewGearSubmission: Input validation with category parsing and empty checks

Timeline Integration:
- Logs "added" events with Category, Make, Model details
- Events created in route handler (route-based approach like Bags)

Route Registration (application/routes/mod.rs):
- API routes: GET/POST /api/v1/gear, GET/PUT/DELETE /api/v1/gear/:id
- Web route: GET /gear

AppState Updates (application/server.rs):
- Add gear_repo field to AppState struct
- Initialize SqlGearRepository in serve() function
- Update AppState::new() to include gear_repo parameter

Supports both JSON API and form submissions with Datastar reactive updates.
2026-02-02 15:21:26 +00:00
Jon Seager
2309a696cc
feat(infrastructure): implement Gear repository and HTTP client
Add SQL repository and HTTP client implementations for Gear entity.

SQL Repository (infrastructure/repositories/gear.rs):
- SqlGearRepository with CRUD operations
- order_clause() for sorting: Make/Model (case-insensitive), Category, CreatedAt
- build_where_clause() for category filtering
- to_domain() converts GearRecord to domain Gear with category parsing
- Uses push_update_field! macro for partial updates
- Proper error handling with RepositoryError types

HTTP Client (infrastructure/client/gear.rs):
- GearClient for CLI access to API endpoints
- Methods: create(), list(), get(), update(), delete()
- Supports optional category filter in list()
- Error context with anyhow for user-friendly messages

Module Registration:
- Register gear module in infrastructure/repositories/mod.rs
- Register gear module and gear() method in infrastructure/client/mod.rs

Follows the exact patterns from BagRepository and BagsClient.
2026-02-02 15:19:19 +00:00
Jon Seager
b794d8a63f
feat(domain): add Gear entity with database migrations
Add Gear entity to track brewing equipment (grinders and brewers) with complete domain layer implementation.

Database changes:
- migrations/0006_add_gear.sql: Create gear table with category CHECK constraint and indexes
- migrations/0007_update_timeline_for_gear.sql: Document 'gear' as valid timeline entity type

Domain layer:
- Add GearId typed ID wrapper
- Create domain/gear.rs with:
  - GearCategory enum (Grinder/Brewer) with string conversion methods
  - Gear entity with make, model, notes fields
  - NewGear and UpdateGear DTOs
  - GearFilter for category-based filtering
  - GearSortKey with Make (default), Model, Category, CreatedAt options
- Add GearRepository trait to domain/repositories.rs with standard CRUD operations
- Register gear module in domain/mod.rs

This follows the same architectural pattern as the Bag entity.
2026-02-02 15:14:23 +00:00
Jon Seager
4bae7f1762
refactor(routes): re-export is_authenticated for shorter imports
Add pub(crate) re-export of is_authenticated from routes/mod.rs,
allowing handlers to use super::is_authenticated instead of the
verbose crate::application::routes::auth::is_authenticated path.

Updated 9 call sites across roasters.rs, roasts.rs, bags.rs, and
timeline.rs.
2026-02-02 14:32:30 +00:00
Jon Seager
7b0b6409e5
refactor(repos): use SQLx is_unique_violation for constraint checks
Replace string matching on "UNIQUE constraint failed" with SQLx's
type-safe is_unique_violation() method in roasters.rs and roasts.rs.

This aligns with users.rs and tokens.rs, and ensures the check works
across different database backends (SQLite, PostgreSQL) without
depending on error message formatting.
2026-02-02 14:30:30 +00:00
Jon Seager
2e4c8722ca
refactor(bags): align order_clause with other repositories
- Use method access (sort_key(), sort_direction()) instead of field
  access for consistency with roasters.rs and roasts.rs
- Add secondary sort clauses (e.g., created_at DESC) as tiebreakers
  to ensure deterministic pagination results
2026-02-02 14:28:45 +00:00
Jon Seager
c941cd83fb
docs(bags): add SAFETY comment for SQL string interpolation
Document why direct format!() interpolation is acceptable in
build_where_clause(): the values are type-safe (bool outputs literal
TRUE/FALSE, roast_id is i64 from typed wrapper). Warns future
developers to use parameterized queries if string fields are added.
2026-02-02 14:27:03 +00:00
Jon Seager
3a9fb16793
refactor(bags): replace multiple list methods with composable filter
- Add BagFilter struct with constructor methods (all, open, closed, for_roast)
- Replace 5 repository methods with single list(filter, request) method
- Add build_where_clause helper for dynamic WHERE clause construction
- Update all callers in bags and roasts routes

This eliminates method explosion when adding new filters - now only
BagFilter and build_where_clause need updating instead of adding
new repository methods.
2026-02-02 14:15:18 +00:00
Jon Seager
2d183e6955
refactor(routes): add macros to reduce route handler boilerplate
- Create define_get_handler! macro for GET-by-ID endpoints
- Create define_delete_handler! macro for DELETE endpoints with Datastar support
- Apply macros to roasters, roasts, and bags route modules
- Reduces 6 handlers from ~78 lines to ~12 lines total
2026-02-02 13:40:14 +00:00
Jon Seager
d689e48328
refactor(infra): standardize SQL query construction across repositories
- Add push_update_field! macro to reduce UPDATE query boilerplate
- Rename sort_clause() to order_clause() for consistency
- Convert bags.rs update method from string concatenation to QueryBuilder
- Apply macro to roasters.rs, roasts.rs, and bags.rs update methods

This reduces ~100 lines of repetitive code and ensures consistent
patterns for building dynamic UPDATE queries across all repositories.
2026-02-02 13:09:33 +00:00
Jon Seager
3e0aa1653f
chore(refactor): define macro for get/delete cli commands 2026-02-02 13:00:48 +00:00
Jon Seager
6611643424
chore: move RepositoryError into dedicated file 2026-02-02 12:57:15 +00:00
Jon Seager
ab9d4bfa13
feat: add amount to Bag Finished events 2025-11-27 15:10:17 +00:00
Jon Seager
f750107bb1
fix: add an action field to timeline events 2025-11-27 14:52:13 +00:00
Jon Seager
144257c2c8
fix: ensure bags are closed properly and closures are on the timeline 2025-11-27 14:42:38 +00:00
Jon Seager
673f76f09a
fix: ensure the list-bags command with no id lists all bags 2025-11-27 14:32:48 +00:00