Add image infrastructure, API, and detail page integration:
- EntityImage domain model with ImageRepository trait
- SQLite storage for images and thumbnails as BLOBs
- Image processing with data URL decoding and thumbnail generation
- REST API: upload, get, delete, thumbnail at /{entity_type}/{id}/image
- resolve_image_url helper for image fallback chains
- image-upload web component for direct upload on detail pages
- Image display with vignette overlay and lightbox viewer
- Template macros: image_thumbnail, readonly_image, lightbox_script
- All 7 detail pages updated with image thumbnails
- Brew images fall back to roast; cup images fall back to cafe then roast
- Delete handler macro extended with optional image cleanup
The grinder_model field was computed by splitting the concatenated
"make model" string on the first space, which broke for multi-word
brand names like "Weber Workshops". Fetch g_grinder.model directly
from the database instead.
Move COUNTRY_MAP, country_to_iso(), iso_to_flag_emoji() and their tests
from country_stats.rs into a new countries.rs module for reuse by the
detail page view models.
- Add format_weight() and format_relative_time() in domain/formatting.rs
- Replace ad-hoc weight format strings across views and timeline events
- Move relative_date() body to domain layer with explicit now param
- Remove hardcoded "g" suffix from bag templates (now in format_weight)
- Document formatting helpers in CLAUDE.md
- New /stats page with Roasters, Roasts, Cups, and Cafes tabs
- Interactive choropleth world map (SVG) colored by country counts
- Clickable country chips that highlight individual countries on the map
- Horizontal chip scroller with chevron navigation on desktop
- Datastar-powered tab switching without page reload
- Domain layer: country name → ISO code mapping, flag emoji generation
- StatsRepository trait with four aggregate SQL queries
- <world-map> and <chip-scroll> custom elements for Datastar compatibility
- Add brew_time INTEGER column to brews table (nullable, stored as seconds)
- Display as M:SS format with stepper control (+/-5s) in add form
- Replace ratio with brew time in brew list Recipe column
- Show brew time in brew cards and timeline events
- Add --brew-time CLI flag and update bootstrap script (1:45-2:45 range)
Cards start collapsed showing category, relative date, title, and
subtitle. Clicking expands to reveal full timestamp, detail rows,
tasting notes, and external links. Uses a single Datastar signal
for expand state.
- Replace pill badge with small-caps category label
- Remove brew-again button from timeline cards
- Replace @ separators with · in brew detail values
Add `created_at: Option<DateTime<Utc>>` through all layers so CLI users
can backdate entities at creation/update time. When omitted, falls back
to `Utc::now()`.
- Domain: add field to all New*/Update* structs with serde(default)
- Domain: timeline events use entity created_at instead of Utc::now()
- Repos: unify INSERT to explicit Rust-side created_at with unwrap_or_else
- Repos: add created_at to UPDATE dynamic query builders
- Routes: add field to submission structs and has_changes guards
- Clients: pass created_at through manual JSON client methods
- CLI: add --created-at flag with parse_created_at helper (RFC 3339 or YYYY-MM-DD)
Add list_all() to PasskeyCredentialRepository so auth_start and
auth_finish fetch all credentials in a single query instead of
looping through every user with a per-user query.
Reject non-http(s) URL schemes (javascript:, data:, etc.) in both
NewRoaster::normalize() and new UpdateRoaster::normalize(). The HTML
input type="url" provides browser-side validation, but the API path
had no server-side check.
- Add spacious flat table CSS for desktop (transparent card wrapper,
lighter dividers, visible hover, larger cell padding)
- Split roaster/cafe columns into separate Country and City columns
with sortable headers
- Split cup Roast/Roaster into separate desktop columns with city
sort support (full-stack: domain, repo, view, template)
- Add sortable Status column to bags with colored pills (green Open,
amber Closed) and remaining weight subtext
- Style bag Finished column as date-only (NaiveDate) and Weight
column with smaller text
- Show status pill next to mobile bag card actions menu
- Add city to cups mobile card view
- Use middle dot separator for brew recipe weight/volume
- Add tasting notes pills to roast list, brew notes to brew list
- Fix build.rs to use rerun-if-changed for CSS and templates
Remove inline signal reset chains before extraction (server already
returns all signals) and before reload (reload destroys signal store).
Change gear and bags default sort from Make/RoastDate to CreatedAt so
data tables show newest entries first.
Predefined toggleable tags for brews: Good, Too Fast, Too Slow,
Too Hot, Under Extracted, Over Extracted.
- Add quick_notes TEXT column storing JSON array
- Add QuickNote enum with label/form-value/positive helpers
- Update repository to encode/decode JSON, backup to export notes
- Add toggle pill UI on brew form with Datastar signals
- Show pills on home page cards (muted for Good, amber for others)
- Show comma-joined text in data table Notes column
- Add --quick-notes flag to CLI, update HTTP client
- Update bootstrap script with sample quick notes
Drop the rating column from cups. This removes the field from the
domain model, repository, API routes (including the PUT/update
endpoint), CLI, and all related tests.
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.
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)
- 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
- 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
- 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
Add `search: Option<&str>` to list methods on RoasterRepository,
RoastRepository, BagRepository, GearRepository, and BrewRepository.
Default trait methods pass None for backwards compatibility.
- 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
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.
- 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
- 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
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
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.
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.
- 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
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.
- 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.
Code Quality Improvements:
- Fix hex literal grouping in ID generator (0xB10C_1D -> 0x00B1_0C1D)
- Rename ListQuery::default() to default_query() to avoid confusion with Default trait
- Use div_ceil() instead of manual ceiling division
- Remove unnecessary borrows in auth token generation and hashing
- Simplify nested if statements in error handling
Documentation:
- Add comprehensive authentication section to README
- Document environment variables for server and CLI
- Add security best practices and considerations
- Document password hashing (Argon2id), token storage (SHA-256), and session management
- Include step-by-step authentication setup guide
- Add production deployment recommendations
All 70 tests pass (8 unit + 46 server + 16 CLI) ✅
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>
- Add sessions table to store session tokens with expiration
- Create Session domain model and SessionRepository trait
- Implement SqlSessionRepository for session persistence
- Update is_authenticated() to validate tokens against database
- Sessions expire after 30 days
- Session tokens hashed with SHA-256 before storage
- Delete sessions from database on logout
- Update all page handlers to properly validate sessions
This prevents session hijacking by ensuring only valid, unexpired
tokens stored in the database can authenticate requests.
Co-authored-by: jnsgruk <668505+jnsgruk@users.noreply.github.com>