Commit graph

105 commits

Author SHA1 Message Date
Jon Seager
766376be12
fix(checkin): use services instead of repos to record timeline events
The check-in handler was calling cafe_repo.insert() and
cup_repo.insert() directly, bypassing the service layer that records
timeline events. Switch to cafe_service.create() and
cup_service.create() so cafes and cups created via check-in appear on
the timeline.

Add timeline tests for cups (existing cafe, new cafe) and brews to
prevent this regression.
2026-02-07 08:28:12 +00:00
Jon Seager
5959f625b5
chore: remove unused favicon.ico 2026-02-06 19:07:20 +00:00
dependabot[bot]
c0d6e588f6
build(deps): bump axum from 0.7.9 to 0.8.8 (#6)
* build(deps): bump axum from 0.7.9 to 0.8.8

Bumps [axum](https://github.com/tokio-rs/axum) from 0.7.9 to 0.8.8.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.7.9...axum-v0.8.8)

---
updated-dependencies:
- dependency-name: axum
  dependency-version: 0.8.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): adapt code for axum 0.8 and tower-cookies 0.11

- Bump tower-cookies 0.10 → 0.11 (requires axum-core 0.5 / axum 0.8)
- Remove axum::async_trait usage (axum 0.8 uses native async traits)
- Migrate route path params from :param to {param} syntax

* fix(deps): correct axum and tower version constraints

Dependabot updated the lockfile for axum 0.8 but left the Cargo.toml
constraint at "0.7", causing CI to resolve back to 0.7.9. It also
incorrectly downgraded tower from "0.5" to "0.4", creating a duplicate
tower version in the lockfile and trait mismatches with axum 0.8.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Seager <jon@sgrs.uk>
2026-02-06 19:04:14 +00:00
Jon Seager
ca43ad1d00
fix: set HTTP client timeout and remove pagination double-fetch
- Set a 30-second default timeout on the shared reqwest client so
  future call sites can't hang indefinitely.
- Use ensure_page_within() to clamp the page before querying instead
  of fetching, detecting an empty result, and re-fetching.
2026-02-06 18:19:37 +00:00
Jon Seager
42b971658d
fix(auth): clean up expired sessions on server startup
Wire the existing delete_expired() method to run at startup so the
sessions table doesn't grow unboundedly with stale rows.
2026-02-06 18:19:30 +00:00
Jon Seager
f2fcada799
perf(auth): fix N+1 queries in WebAuthn authentication
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.
2026-02-06 18:18:58 +00:00
Jon Seager
4fb2e945a4
perf(http): add gzip compression and static asset cache headers
- Add CompressionLayer with gzip to the middleware stack
- Add Cache-Control: public, max-age=604800 (1 week) to all static
  asset routes (CSS, JS, favicons)
2026-02-06 18:05:09 +00:00
Jon Seager
b4f9c02e5a
fix(brews): return reload response for brew-again from non-brew pages
The create_brew handler returned a #brew-list fragment for all Datastar
requests, but that element only exists on the brews data page. Requests
from the homepage or timeline now get a reload-script response instead,
fixing the PatchElementsNoTargetsFound error.
2026-02-06 17:52:28 +00:00
Jon Seager
830d424297
fix(security): add server-side URL scheme validation for roaster homepage
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.
2026-02-06 17:52:15 +00:00
Jon Seager
533a0a067f
fix(security): make session insert synchronous and default cookie Secure flag to on
- Await session DB insert before setting cookie, preventing orphaned sessions
- Invert cookie Secure default: now on by default, opt out via
  BREWLOG_INSECURE_COOKIES=true for local HTTP development
2026-02-06 17:52:01 +00:00
Jon Seager
85946ba9a4
fix(security): add security response headers and request body size limit
- Add Content-Security-Policy, X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, and Strict-Transport-Security response headers
- Set explicit 5 MB request body size limit via RequestBodyLimitLayer
- Enable tower-http "limit" and "set-header" features
2026-02-06 17:51:53 +00:00
Jon Seager
3e0c2ae771
feat(ui): add theme-aware SVG favicon that follows light/dark mode
- Create light (orange-700) and dark (orange-600) coffee cup SVG favicons
- Add explicit routes for both SVG variants
- Set correct favicon on initial load before body renders
- Swap favicon href on manual theme toggle
- Listen for OS prefers-color-scheme changes when no manual override set
2026-02-06 16:44:00 +00:00
Jon Seager
9a46bb3613
refactor: update AppState imports to use state module
Mechanical change: all route handlers and auth middleware now import
AppState from application::state instead of application::server.
2026-02-06 13:48:09 +00:00
Jon Seager
4909d24a60
refactor(server): extract AppState into dedicated state module
Move AppState, AppStateConfig, and from_database() out of server.rs
into state.rs so server.rs only contains startup and shutdown logic.
2026-02-06 13:47:59 +00:00
Jon Seager
202dafb9b9
feat(scan): show cards for existing roasters/roasts during bag scan
After AI extraction, check if the roaster and roast already exist by
slug. When matched, show compact summary cards instead of full edit
forms. Each card has a "Change" link to revert to the form if the
match is wrong.

- Add match_existing_entities() for slug-based roaster/roast lookup
- Return _matched-roaster-id and _matched-roast-id signals from extraction
- Add submit_existing_roast() path to skip creation when roast exists
- Dynamic submit buttons: Save Roaster & Roast / Save Roast / Open Bag
- Hidden inputs bound to signals handle all form submission cleanly
2026-02-06 12:27:25 +00:00
Jon Seager
4a7b054de1
feat(brews): pre-select bag when clicking Brew on homepage bag card
- Add initial-value attribute to searchable-select web component
- Update bag card Brew link to pass bag_id query parameter to add page
- Thread bag_id through route handler and template to pre-select the bag
2026-02-06 10:53:42 +00:00
Jon Seager
0030f87eed
refactor(timeline): extract event creation into service layer
Introduce application services that encapsulate entity creation +
timeline event recording. Route handlers call service.create() instead
of repo.insert() + inline timeline construction.

- Add define_simple_service! macro for roaster/cafe/gear services
- Add custom services for roasts, bags, brews, cups (need enrichment
  or cross-entity lookups)
- Add to_timeline_event() methods to all 7 domain entity types
- Remove timeline SQL from 4 repository insert() methods
- Delete brew_timeline_event() helper from brews route handler
- Update backup test to use services (timeline events created naturally)
- Document service layer pattern in CLAUDE.md
2026-02-06 10:42:52 +00:00
Jon Seager
4e4ac30332
refactor(account): convert token creation from imperative JS to Datastar
Replace fetch/classList DOM manipulation with Datastar signals and @post.
Server now returns signal patches for Datastar requests while preserving
the JSON API for CLI usage.
2026-02-05 21:58:57 +00:00
Jon Seager
f1cdf75eaf
refactor(templates): replace manual JS with Datastar patterns
- Convert data.html search from manual fetch+debounce to Datastar @get
- Replace filterList() on checkin page with <searchable-select> component
- Convert account.html form show/hide from classList to data-show signals
- Rename _scanWaiting/_scanError to _extracting/_extractError for consistency
- Delete unused render_signals_fragment() and escape_html_attr() from support.rs
- Document Datastar vs JS guidelines in CLAUDE.md
2026-02-05 21:42:14 +00:00
Jon Seager
1377d43935
fix: resolve clippy warnings for dead field and struct prefix
- Remove unused passkey_name from RegisterStartRequest (only used in RegisterFinishRequest)
- Rename AiUsageView fields from total_calls/total_tokens/total_cost to calls/tokens/cost
2026-02-05 20:49:17 +00:00
Jon Seager
010b700595
feat(brews): add quick notes
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
2026-02-05 20:42:03 +00:00
Jon Seager
a9d97d40bd
refactor(ai): make OpenRouter URL configurable for testability
Add url parameter to ai::extract_roaster(), extract_roast(), and
extract_bag_scan() instead of using a hardcoded constant. Wire through
AppState.openrouter_url (defaulting to the production URL), matching
the existing pattern used for foursquare_url.

Add test infrastructure: session creation helper, page_url() on
TestApp, and spawn_app_with_openrouter_mock() for wiremock-based
AI extraction testing.
2026-02-05 19:18:51 +00:00
Jon Seager
b4a885e990
refactor(routes): simplify visibility to pub(crate)
Replace verbose pub(in crate::application::routes) with pub(crate)
across all api/ modules and support.rs for readability.
2026-02-05 19:08:44 +00:00
Jon Seager
f25101eef4
refactor(routes): wire up api/app module structure
Create api/mod.rs with API router and webauthn router construction.
Create app/mod.rs with page router and static asset handlers. Rewrite
routes/mod.rs to combine api::router() + app::router(). Widen
support.rs load_*_options visibility to pub(in crate::application::routes).
2026-02-05 19:05:03 +00:00
Jon Seager
58b551afac
refactor(routes): split account, checkin, and webauthn into api/app
Split each mixed file along the api/page boundary:
- account.rs: page handler + view types to app/, API handlers to api/
- checkin.rs: checkin_page to app/, submit_checkin to api/
- webauthn.rs: register_page + cli_callback_page to app/
2026-02-05 19:04:52 +00:00
Jon Seager
bb8fff2630
refactor(routes): move page handlers to routes/app/ subdirectory
Move home, auth, data, add, and timeline page handlers to routes/app/.
Update import paths: super::is_authenticated to
crate::application::routes::is_authenticated, super::entity:: to
crate::application::routes::api::entity:: in data.rs and add.rs.
2026-02-05 19:04:36 +00:00
Jon Seager
9cc0b30cca
refactor(routes): move API handlers to routes/api/ subdirectory
Move entity CRUD modules (roasters, roasts, bags, brews, gear, cafes,
cups), pure API modules (tokens, backup, scan), macros, and webauthn
to routes/api/. Remove page handlers from api/webauthn.rs. Update
import paths (super::support:: to crate::, scan's TastingNotesInput
reference to super::roasts::). Widen pub(super) to
pub(in crate::application::routes) on page loaders and data structs
for cross-subdirectory access.
2026-02-05 19:04:21 +00:00
Jon Seager
fe4cb4ffbb
feat(logging): add CRUD operation and timeline error logging
- Add info! logging for entity create/update/delete across all route handlers
- Add entity deleted logging in define_delete_handler! macro
- Log timeline insert errors instead of silently discarding them
2026-02-05 18:14:00 +00:00
Jon Seager
4ab00dd630
refactor(logging): log errors and security events in auth routes
- Log all map_err(|_| StatusCode) patterns in webauthn, tokens, and account handlers
- Log fire-and-forget errors: registration token mark_used, passkey credential
  counter update, passkey update_last_used
- Add security event logging: token create/revoke, passkey delete, login/logout,
  CLI token creation via passkey auth
- Log session delete failures and auth check errors
2026-02-05 18:13:47 +00:00
Jon Seager
5ee89549a3
refactor(logging): log errors in auth and payload parsing
- Log bearer token lookup and user lookup failures in AuthenticatedUser extractor
- Log session lookup and user lookup failures in session-based authentication
- Log fire-and-forget token update_last_used errors
- Log JSON and form payload parse failures in FlexiblePayload
2026-02-05 18:13:34 +00:00
Jon Seager
06384265d3
refactor(logging): switch to env-based log format with HTTP tracing
- Replace tracing-bunyan-formatter with tracing-subscriber's built-in fmt layer
- Default to compact human-readable format, JSON opt-in via RUST_LOG_FORMAT=json
- Add tower-http TraceLayer for automatic HTTP request/response logging
- Remove tracing-bunyan-formatter and tracing-log dependencies
2026-02-05 18:13:19 +00:00
Jon Seager
56109f3afe
refactor: reorganize templates into pages/, partials/lists/, and forms/
Move 9 page templates to templates/pages/, nav to partials/,
7 list partials + table.html to partials/lists/. Extract duplicated
scan result form (~85 lines) from home.html and add.html into
partials/forms/scan_result_form.html. Update all Askama template
paths, include/import directives.
2026-02-05 17:48:18 +00:00
Jon Seager
4e917cee75
refactor: move static assets out of templates/ into static/
Move CSS, JS, favicon files to static/ directory to separate
compiled-in assets from Askama templates. Update all include paths,
build.rs, flake.nix, and Tailwind @source directives.
2026-02-05 17:47:32 +00:00
Jon Seager
4a54834424
refactor(ui): extract reusable tab bar component
- Create shared tab_bar.html partial with desktop + mobile layouts
- Add CSS component classes (.tab, .tab-active, .tab-mobile variants)
- Replace divergent implementations in data.html and add.html
- Rename DataTab to Tab for shared use across templates
- Improve readability: darker inactive text, more spacing, hover states
2026-02-05 17:23:50 +00:00
Jon Seager
fb67f0659c
feat(ui): redesign with theme tokens, dark mode, and reusable components
Overhaul the web UI with CSS custom property-based theming and
dark mode support. Extract reusable template partials (location
search, scan input) and web components (photo-capture,
searchable-select). Add version and commit info to page footer.

- Replace hardcoded amber palette with semantic theme tokens
- Add dark mode with localStorage persistence and system preference detection
- Extract <brew-photo-capture> and <searchable-select> web components
- Extract location_search and scan_input Askama macros
- Add sun/moon, timeline, database, map icons
- Display version and git commit in footer
- Improve timeline coordinate rounding and map link formatting
- Add roast_name, roaster_name, remaining to BagOptionView
2026-02-05 17:03:09 +00:00
Jon Seager
6d12b71482
refactor(cups): remove rating field
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.
2026-02-05 17:02:42 +00:00
Jon Seager
149921dc31
refactor(account): simplify backup/restore with native download link
Replace the blob URL download JS with a Content-Disposition header on
the export endpoint and a plain <a> tag. Consolidate restore JS from
event listeners into a single global function.
2026-02-05 12:27:17 +00:00
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
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
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