Commit graph

132 commits

Author SHA1 Message Date
Jon Seager
aad927085b
refactor: extract location JS into static file with nearby keyboard nav
Move locateUser and nearbyKeydown functions from an Askama macro into
static/js/location.js served as a deferred script. Add arrow key, Enter,
and Escape navigation for nearby cafe search results.
2026-02-09 19:54:49 +00:00
Jon Seager
000f20eab7
feat(server): auto-enable insecure cookies for localhost defaults
Default BREWLOG_RP_ID to "localhost" and BREWLOG_RP_ORIGIN to
"http://localhost:3000". When these localhost defaults are active,
insecure cookies are enabled automatically so local dev works without
setting BREWLOG_INSECURE_COOKIES. A warning is logged at startup when
insecure cookies are active.

The insecure_cookies flag is now threaded through AppState instead of
reading the env var at cookie-set time.
2026-02-09 17:23:09 +00:00
Jon Seager
edc0021792
refactor: split large directories into coffee/auth/analytics subdirs
- Restructure domain/, repositories/, and routes/api/ into themed
  subdirectories: coffee/, auth/, analytics/ (and system/ for routes)
- Convert super:: relative imports to crate:: absolute paths
- Promote macro visibility from pub(super) to pub(crate)
- Add pub use re-exports for backward-compatible import paths
2026-02-08 18:16:16 +00:00
Jon Seager
0fd4214b6c
refactor(static): prefix all static asset routes with /static/
Move static asset URLs from root-level paths to /static/ prefix
mirroring the filesystem layout (e.g. /styles.css → /static/css/styles.css,
/components/*.js → /static/js/components/*.js).
2026-02-08 18:07:10 +00:00
Jon Seager
f0eb346086
feat(detail): add roast detail page, simplify lists and actions
- Add roast detail page at /roasters/{slug}/roasts/{slug}
- Remove expand/collapse detail rows from all 7 list views
- List rows now navigate directly to entity detail pages
- Replace three-dots action button with chevron-right link
- Add delete buttons to brew and cup detail pages
- Restyle all delete buttons: outlined with red text
- Remove share buttons from all detail pages
- Update timeline card links to point at detail pages
- Make homepage activity cards clickable with hover effect
- Replace all vanilla JS delete/close with Datastar actions
- Extract render_redirect_script helper for Datastar redirects
- Update delete macro with referer-based routing for detail pages
2026-02-08 17:48:49 +00:00
Jon Seager
958c8de6cc
feat(detail): add roaster, cafe, and gear detail pages
- Add detail page routes, templates, and view models for roasters, cafes,
  and gear, following the existing pattern from bags/brews/cups
- Add map_with_legend_1 macro for single-country detail maps
- Redirect to detail page after entity creation instead of list page
- Check referer in create handlers to return list fragment only from data page
- Update bag delete button to use red background style
- Update datastar tests to include referer header for create assertions
2026-02-08 17:20:46 +00:00
Jon Seager
ccbd229d75
style(stats): add bg-surface to cards/pills/tabs, reorder tabs
- Add bg-surface background to roast stats, consumption, and country pill cards
- Update tab CSS: bg-surface background, hover:border-accent/40 instead of bg-surface-alt
- Add hover:border-accent/40 to clickable country pill buttons
- Make Roasts the first and default tab on the stats page
2026-02-08 16:47:30 +00:00
Jon Seager
4fd72d654a
feat(bags): add bag detail page, extract shared detail page macros
- Create /bags/:id detail page with coffee, roaster, map, and bag info cards
- Extract shared template macros into detail_cards.html (coffee_card, roaster_card, map_with_legend, share_button)
- Extract build_coffee_info() and build_roaster_info() view model helpers
- Refactor brew.html and cup.html to use shared macros
- Make bag cards on homepage clickable, linking to detail page
- Add Close Bag and Delete actions on bag detail page
- Unify homepage card styling (bg-surface, hover:border-accent/40)
- Update CLAUDE.md with detail page patterns
2026-02-08 16:43:04 +00:00
Jon Seager
9e5dff7f5f
feat(web): add Open Graph and Twitter Card meta tags
- Add og:title, og:description, og:image, og:type, og:site_name and
  twitter:card meta tags to base template with Askama block overrides
- Create static 1200x630 OG image served at /og-image.png
- Override OG blocks with entity-specific content on brew, cup, and
  stats detail pages
- Reuse BREWLOG_RP_ORIGIN as base URL for absolute og:image URLs via
  OnceLock initialized at server startup
2026-02-08 16:19:34 +00:00
Jon Seager
f5e319ce03
feat(ui): add empty states for stats and data sections
Show blurred placeholder cards with "No stats yet" / "No data yet"
labels when no data exists, instead of hiding the sections entirely.
2026-02-08 16:10:32 +00:00
Jon Seager
eaf558a524
feat(routes): redirect to detail page after creating brews and cups
Update create_brew, create_cup, and submit_checkin to navigate to the
new detail pages instead of reloading or redirecting to home.
2026-02-08 16:10:11 +00:00
Jon Seager
b17aaab5f4
feat(detail): add shareable brew and cup detail pages
Add /brews/:id and /cups/:id routes with full coffee, roaster, gear,
and recipe information. Each page includes a world map highlighting
relevant countries, tasting note pills, and a share button that copies
the URL to the clipboard.
2026-02-08 16:10:01 +00:00
Jon Seager
bfe65d23ff
feat: add /health endpoint for Fly.io health checks
- Add lightweight GET /health returning {"status":"ok"}
- Update fly.toml to use /health instead of /
2026-02-08 15:37:35 +00:00
Jon Seager
dbdb15a492
feat(home): add stats cards and horizontal scroll to data section
- Add build_stat_cards() with shuffled order from stats cache
- Remove gear from homepage data cards and StatsView
- Add StatCard view struct for icon/value/label display
- Convert data cards from grid to horizontal scroll with chip-scroll
- Add stats section with 6 randomized stat cards
2026-02-08 15:21:19 +00:00
Jon Seager
66c5cd0d6c
feat(stats): add full stats page with consumption, brewing, and charts
- Refactor stats handler to load_or_compute with cache fallback
- Add donut charts for brewer and grinder usage (with XSS-safe labels)
- Add bar charts for origins, flavours, grind weight, and brew time
- Add consumption cards (total brewed, avg per brew, bags consumed)
- Add roast summary section (top origins, processes, producers)
- Register donut-chart.js static asset route
- Add Recompute Stats button to admin page
2026-02-08 15:20:29 +00:00
Jon Seager
97c00f2e33
feat(stats): add pre-computed stats cache with background recomputation
- Add CachedStats domain types (roast summary, consumption, brewing)
- Extend StatsRepository with summary queries and cache get/store
- Add StatsInvalidator + background task with 2s debounce
- Add invalidate() calls to all entity create/update/delete handlers
- Add POST /api/v1/stats/recompute endpoint for manual refresh
- Add stats_cache migration and include in database reset
- Add GeoStats::from_counts() and Serialize/Deserialize derives
- Add 6 integration tests for stats API
- Document Stats Cache pattern in CLAUDE.md
2026-02-08 15:20:01 +00:00
Jon Seager
585eb77799
feat(home): add floating chevron scroll buttons to card sections
Extract <chip-scroll> custom element into its own file and wrap
Recent Brews and Open Bags horizontal scrollers with desktop-only
chevron navigation that half-overlaps the card edges.
2026-02-08 11:51:13 +00:00
Jon Seager
7f99ffc993
feat(stats): add geographic stats page with choropleth world map
- 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
2026-02-08 11:03:15 +00:00
Jon Seager
3f672d7c06
chore: fix lints 2026-02-08 09:40:37 +00:00
Jon Seager
35e1812879
refactor: rename Account to Admin across filenames, routes, and docs
- Rename account.{rs,html} → admin.{rs,html} in app, api, and templates
- Update route /account → /admin
- Rename AccountTemplate → AdminTemplate, account_page → admin_page
- Update nav link, test function names, and CLAUDE.md references
2026-02-08 09:39:37 +00:00
Jon Seager
df4469ff69
feat(brews): make Brew Again navigate to pre-filled add form
Instead of immediately creating a duplicate brew via inline POST,
the Brew Again button now links to the add-brew page with query
params that pre-fill all fields (bag, grinder, grind setting,
brewer, filter paper, water volume/temp, brew time, quick notes).
This lets users review and adjust parameters before brewing.
2026-02-07 19:57:23 +00:00
Jon Seager
51f8122201
feat(ui): improve form UX with collapsible equipment and clickable controls
- Split equipment into separate Grinder and Brewer collapsible sections
  with summary bars showing current selection
- Make all summary bars, matched-entity cards, and searchable-select
  selected values clickable anywhere to change (not just the X/Change button)
- Fix stepper buttons broken after manual input by switching from
  data-attr:value to data-bind with Number() coercion
- Merge Water + Brew Time into single Recipe section
- Make all add/confirm buttons full-width
- Add sticky submit button on mobile
- Fix missing closing > on cafe indicator div
- Fix broken Askama syntax on checkin page from formatter
2026-02-07 12:10:56 +00:00
Jon Seager
902d52b171
feat(brews): add brew time field
- 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)
2026-02-07 11:24:07 +00:00
Jon Seager
577c0e39d3
feat(ui): add horizontal scrolling recent brews on homepage
- Load last 10 brews instead of 3, display in horizontal scroll
- Extract brew card into reusable partial with portrait layout
- Add grinder_model field to BrewView for compact card display
- Move brew-again button to top-right icon, truncate long names
2026-02-07 10:40:20 +00:00
Jon Seager
8410b46a11
feat(ui): add horizontal scrolling open bags on homepage
- Load all open bags instead of limiting to 3
- Replace grid layout with horizontal scroll container using snap
- Redesign bag cards as portrait layout with icon actions
- Add scrollbar-hide CSS utility and x_mark icon
2026-02-07 10:22:48 +00:00
Jon Seager
e01b6d0a1c
feat: add optional created_at to entity creation and updates
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)
2026-02-07 09:48:20 +00:00
Jon Seager
e58ea5758f
feat(backup): add database reset action to account page
- Add reset() method to BackupService that deletes all 8 coffee tables
  in a transaction, respecting FK constraint order
- Add POST /api/v1/backup/reset endpoint (requires auth)
- Add "Reset Database" button with double-confirmation to account page
- Stack data buttons vertically on mobile for better layout
2026-02-07 08:45:30 +00:00
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