Commit graph

56 commits

Author SHA1 Message Date
Niels Göttsch
9cf8ce5d79 feat(auth): generate additional invite links for onboarding users
Add an authenticated POST /api/v1/invites endpoint that mints a
single-use, 7-day registration link after the first-user bootstrap.
Expose it via a new "Invite" section on the admin page and a
`brewlog invite create` CLI command.

The registration_tokens table, repository, and NewRegistrationToken
already supported additional tokens; this adds the authenticated
surfaces (session cookie or bearer token) to trigger creation.
2026-07-16 12:38:36 +02:00
Jon Seager
937ee4c5f2
feat: store bag finished_at as full datetime for correct timeline ordering
The finished_at field previously stored only a date, causing bag "finished"
timeline events to sort before same-day brews. Now stores a full datetime
and uses the actual close time for timeline ordering.
2026-02-24 21:38:39 +00:00
Jon Seager
76da3d9bff
fix: require discoverable credentials for passkey registration
The webauthn-rs `start_passkey_registration` sets residentKey to
"discouraged", which iOS Safari respects strictly — creating
non-discoverable credentials that never appear in autofill. Desktop
password managers ignore this flag. Patch the creation challenge to
require resident keys so discoverable passkeys work on all platforms.
2026-02-15 12:29:07 +00:00
Jon Seager
5a0321b226
feat: add WebAuthn Conditional UI for passkey autofill
Enables password managers (1Password, iCloud Keychain) to offer passkey
suggestions via autofill on the login page, matching behavior of other
passkey-enabled websites.
2026-02-15 11:38:53 +00:00
Jon Seager
0e3847ade9
feat: sync timeline events with entity edits
Add background timeline rebuild task (mirroring stats cache pattern)
that refreshes denormalized timeline event snapshots when entities are
updated. Includes cascade logic so editing a roaster refreshes timeline
events for its roasts, bags, brews, and cups.

- Add update_by_entity/delete_by_entity/delete_all to TimelineEventRepository
- Add TimelineInvalidator with debounced background rebuild task
- Add invalidate() calls to all 7 entity update handlers
- Add delete_by_entity cleanup to define_delete_handler! macro
- Add gear_id filter to BrewFilter for cascade traversal
- Add `brewlog timeline rebuild` CLI command for full rebuild
- Add 5 integration tests for timeline sync behavior
2026-02-15 11:11:07 +00:00
Jon Seager
12dcc37cbe
fix: persist session cookie with max-age for Safari PWA support
Session cookie lacked a Max-Age attribute, making it ephemeral. Safari's
standalone PWA mode aggressively clears session cookies on app suspend,
causing frequent logouts. Set Max-Age to 30 days to match the server-side
session lifetime.
2026-02-15 10:36:05 +00:00
Jon Seager
472e851b80
refactor: standardize view model conversions on From trait
Convert single-argument from_domain() methods to impl From<T> for
consistency with the existing CafeView, RoasterView, and option view
patterns. Affected: BagView, BrewView, CupView, GearView,
GearDetailView, CafeDetailView, RoasterDetailView, TimelineEventView.

RoastView retains from_domain() since it takes extra parameters.
2026-02-13 16:20:27 +00:00
Jon Seager
de49f7c3b1
refactor: add EntityType enum and typed IDs in TimelineBrewData
Replace stringly-typed entity references with a compile-time-safe
EntityType enum throughout timeline events, images, and repository
operations. Also replace raw i64 fields in TimelineBrewData with
typed BagId/GearId wrappers.
2026-02-13 16:20:14 +00:00
Jon Seager
c58c60c783
fix: security hardening and code health improvements in domain layer
- Remove danger-allow-state-serialisation feature from webauthn-rs
- Add #[serde(skip_serializing)] to Session and RegistrationToken hash fields
- Add custom Debug impls to redact hashes in Session and NewToken
- Add MAX_SESSION_DURATION (30d) and MAX_TOKEN_DURATION (7d) with clamping
- Add domain-level username validation (length + character constraints)
- Extract shared normalize_optional_field to coffee/mod.rs (DRY)
- Implement FromStr for QuickNote, delegate from_str_value to it
- Refactor UpdateRoaster/UpdateCafe normalize() to use shared helper
2026-02-13 16:20:11 +00:00
Jon Seager
a1aefda860
refactor: use update_response helper in update handlers
Replace duplicated datastar/form/json response branching in brew, cup,
and roast update handlers with the shared update_response helper.
2026-02-13 13:05:50 +00:00
Jon Seager
ff2c602139
fix: validate cafe URL scheme on create and update
Make is_valid_url_scheme pub(crate) and use it to filter cafe website
URLs on both create and update, matching roaster validation behavior.
Add UpdateCafe::normalize() method for consistent sanitization.
2026-02-13 13:05:29 +00:00
Jon Seager
0bf30d3475
fix: verify authenticated user in passkey add finish
Add AuthenticatedUser extractor to passkey_add_finish and verify the
session user matches the challenge owner, preventing one user from
completing another user's passkey registration.
2026-02-13 13:05:14 +00:00
Ubuntu
7e0b910008
fix: validate CLI callback URLs to localhost-only
Reject non-localhost CLI callback URLs in the WebAuthn auth start
endpoint to prevent token exfiltration to external servers.
2026-02-13 13:01:12 +00:00
Jon Seager
4190fc2620
feat: log payload fields in trace spans by redacting image data
Add ImageData newtype that wraps Option<String> with a custom Debug impl
showing Some(<image>)/None instead of raw base64. Replace image fields on
all 14 submission structs and remove payload from tracing skip lists so
textual/numeric fields appear in spans.
2026-02-11 08:37:03 +00:00
Jon Seager
2cd579a574
refactor: deduplicate update handlers and edit templates
Add HasChanges trait with impl_has_changes! macro, validate_update()
and update_response() helpers to reduce boilerplate across all 7 entity
update handlers. Extract edit form actions (error, spinner, buttons)
into a shared Askama macro. Also adds missing no-changes validation
to the bag update handler.
2026-02-10 20:04:24 +00:00
Jon Seager
4ee8cf7964
fix: polish edit forms and detail pages
- Skip payload in tracing::instrument to avoid logging base64 image data
- Add blob: to CSP img-src for image preview support
- Add deferred_upload_with_preview macro for edit form image previews
  with Replace/Remove buttons and proper DOM cleanup on replacement
- Fix datastar-fetch finished handler (evt.detail.response is undefined
  for redirect scripts)
- Display brew time in M:SS format on edit form
- Add full-width Save Changes button with check icon and Cancel button
  to all edit forms
- Fix country flag emoji spacing on cafe and cup detail pages
- Add "View on Map" Google Maps link to cafe and cup detail pages
2026-02-10 19:43:03 +00:00
Jon Seager
9650735057
fix: wire edit forms for Datastar submission and recompute bag remaining
Add {contentType: 'form'} to all edit template @put() calls so form
inputs are submitted by name rather than as Datastar signals. Replace
the brew edit quick notes text input with toggle pill buttons. Recompute
bag remaining when amount is updated to account for brew deductions.
Add CLI tests for brew and cup update commands.
2026-02-10 19:43:00 +00:00
Jon Seager
1c029f52f4
feat: add update endpoints for brews and cups
Add UpdateBrewSubmission and UpdateCupSubmission types with image
support. Register PUT handlers on /brews/{id} and /cups/{id} with
three-way response pattern (Datastar/form/JSON).
2026-02-10 19:42:45 +00:00
Jon Seager
bc449e520e
feat: upgrade update handlers to FlexiblePayload with three-way response
All five existing update handlers (roaster, roast, cafe, gear, bag) now
accept FlexiblePayload with UpdateSubmission types that separate image
data from domain structs. Each returns Datastar redirect scripts, form
redirects, or JSON depending on request type. Image save support added
to all update paths.
2026-02-10 19:42:43 +00:00
Jon Seager
77cb61f91b
feat: add UpdateBrew, UpdateCup structs and expand UpdateBag
Add domain update structs for brew and cup entities, add update()
methods to their repository traits and SQL implementations, and
expand UpdateBag with roast_id, roast_date, and amount fields.
2026-02-10 19:42:39 +00:00
Jon Seager
bb4aa103fb
fix: add success logging for deferred image saves
Log at info level when a deferred image is successfully saved, matching
the project convention that every create/update operation logs with
entity context.
2026-02-10 17:50:46 +00:00
Jon Seager
dfd9814fa5
fix: add concurrency semaphore for image processing
Limit concurrent image processing tasks to 4 via a tokio::Semaphore on
AppState. Acquired before spawn_blocking in both upload_image() and
save_deferred_image() to prevent CPU/memory exhaustion from concurrent
image uploads.
2026-02-10 17:50:42 +00:00
Jon Seager
082b582cb3
fix: move image processing off async runtime with spawn_blocking
CPU-intensive image operations (decode, Lanczos3 resize, JPEG encode)
were running directly on the async worker thread, blocking all other
requests for 100-500ms per upload. Wrap in spawn_blocking in both
upload_image() and save_deferred_image().
2026-02-10 17:50:34 +00:00
Jon Seager
c3781de30c
feat: add image upload to create and check-in forms
Add deferred image upload support to entity creation flows:

- Roaster, roast, gear, cafe forms save images on create via
  save_deferred_image helper
- Brew form accepts optional image upload
- Check-in form accepts optional cup photo
- Scan flow preserves captured image for new roasts and skips
  overwriting existing roast images
- Homepage updated with image-upload component registration
- Delete handlers for brews and cups clean up associated images
2026-02-10 17:50:20 +00:00
Jon Seager
4b3f03f5d3
feat: add entity image support with display on detail pages
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
2026-02-10 17:50:13 +00:00
Jon Seager
f6b51ccb38
refactor: reduce duplication in handlers, templates, and view structs
- Replace inline redirect scripts with existing render_redirect_script()
  in 6 create handlers
- Add delete_button macro to detail_cards.html, used by 6 detail pages
- Unify 3 map_with_legend macros into 1 with LegendEntry iteration
2026-02-10 09:37:49 +00:00
Jon Seager
3c273feb18
feat(a11y): add ARIA attributes, token colors, toast, and tab-switch links
Add aria-required to required inputs, role=alert to error messages,
aria-label to icon-only list nav buttons, and progressbar ARIA to bag
status bars. Replace hardcoded red/green colors with error/success
design tokens across all templates. Add toast notifications for entity
creation and tab-switch links for empty add-form states. Increase scan
upload body limit to 10MB.
2026-02-09 19:56:11 +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
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
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
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
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
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
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
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
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
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
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
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
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