docs: update CLAUDE.md and README.md for recent refactors

- Update architecture tree for coffee/auth/analytics subdirectory split
- Fix infrastructure/ai/ (dir) to infrastructure/ai.rs (file)
- Fix stale file paths in gotchas #8, #11, #12
- Update OG image path from /og-image.png to /static/og-image.png
- Expand detail pages section from 3 to 7 pages
- Add map_with_legend_1 macro, fix Used-by columns
- Document <donut-chart> web component
- Add check-in, scanning, timeline, stats to README feature list
This commit is contained in:
Jon Seager 2026-02-08 18:30:25 +00:00
parent edc0021792
commit d2ff4eb854
No known key found for this signature in database
2 changed files with 76 additions and 32 deletions

View file

@ -58,20 +58,35 @@ The codebase follows **Clean Architecture / Domain-Driven Design** with four lay
src/ src/
├── domain/ # Pure business logic, no external dependencies ├── domain/ # Pure business logic, no external dependencies
│ ├── errors.rs # RepositoryError enum │ ├── errors.rs # RepositoryError enum
│ ├── ids.rs # Typed ID wrappers (RoasterId, RoastId, BagId, BrewId, GearId, etc.) │ ├── ids.rs # Typed ID wrappers (RoasterId, RoastId, BagId, BrewId, GearId, CafeId, CupId, etc.)
│ ├── listing.rs # Pagination & sorting (SortKey, ListRequest, Page, PageSize) │ ├── listing.rs # Pagination & sorting (SortKey, ListRequest, Page, PageSize)
│ ├── repositories.rs # Repository traits │ ├── repositories.rs # Repository traits
│ └── {entity}.rs # Entity definitions (roasters, roasts, bags, brews, gear, etc.) │ ├── countries.rs # Country name → ISO code mapping, flag emoji conversion
│ ├── formatting.rs # Shared display formatting helpers
│ ├── coffee/ # roasters, roasts, bags, brews, cups, gear, cafes
│ ├── auth/ # users, sessions, tokens, passkey_credentials, registration_tokens
│ └── analytics/ # timeline, stats, country_stats, ai_usage
├── infrastructure/ # External integrations (database, HTTP clients, third-party APIs) ├── infrastructure/ # External integrations (database, HTTP clients, third-party APIs)
│ ├── repositories/ # SQL implementations of repository traits │ ├── repositories/ # SQL implementations of repository traits
│ │ ├── coffee/ # Coffee entity repos
│ │ ├── auth/ # Auth repos
│ │ └── analytics/ # Analytics repos (timeline_events, stats, ai_usage)
│ ├── client/ # HTTP client for CLI │ ├── client/ # HTTP client for CLI
│ ├── ai/ # OpenRouter LLM integration for AI extraction │ ├── ai.rs # OpenRouter LLM integration for AI extraction
│ ├── foursquare.rs # Foursquare Places API for nearby cafe search │ ├── foursquare.rs # Foursquare Places API for nearby cafe search
│ ├── backup.rs # Database backup/restore
│ ├── webauthn.rs # WebAuthn/passkey credential storage
│ └── database.rs # Database pool abstraction │ └── database.rs # Database pool abstraction
├── application/ # HTTP server, routes, middleware, services ├── application/ # HTTP server, routes, middleware, services
│ ├── routes/ # Axum route handlers │ ├── routes/ # Axum route handlers
│ │ ├── api/ # REST API
│ │ │ ├── coffee/ # Entity CRUD + extract + scan + checkin
│ │ │ ├── auth/ # Tokens, WebAuthn
│ │ │ ├── analytics/ # Stats
│ │ │ └── system/ # Admin, backup
│ │ └── app/ # Web UI page routes
│ ├── services/ # Entity services (create + timeline orchestration) │ ├── services/ # Entity services (create + timeline orchestration)
│ └── errors.rs # HTTP error mapping │ └── errors.rs # HTTP error mapping
@ -109,15 +124,15 @@ Only reset state on `finished` or `error`, never unconditionally.
**7. Use token-based text classes, never hardcoded `text-stone-*`.** Always use `text-text`, `text-text-secondary`, `text-text-muted` which adapt between light and dark themes. **7. Use token-based text classes, never hardcoded `text-stone-*`.** Always use `text-text`, `text-text-secondary`, `text-text-muted` which adapt between light and dark themes.
**8. Static assets need explicit routes and cache headers.** All assets are embedded at compile time via `include_str!()`/`include_bytes!()` with explicit routes in `application/routes/app/mod.rs`. There is no `tower-http` static file serving. Every static asset handler must return a `cache-control: public, max-age=604800` header alongside `content-type`. **8. Static assets need explicit routes and cache headers.** All assets are embedded at compile time via `include_str!()`/`include_bytes!()` with explicit routes in `application/routes/app/mod.rs`. All asset routes use the `/static/` prefix (e.g., `/static/css/styles.css`, `/static/js/components/world-map.js`). There is no `tower-http` static file serving. Every static asset handler must return a `cache-control: public, max-age=604800` header alongside `content-type`.
**9. CSP must be updated when adding external resources.** The `Content-Security-Policy` header is set in `application/routes/mod.rs`. If you add a new external script, stylesheet, font, or image source, update the corresponding CSP directive (`script-src`, `style-src`, `font-src`, `img-src`) or the browser will block it silently. Datastar requires `'unsafe-inline'` and `'unsafe-eval'` in `script-src`. **9. CSP must be updated when adding external resources.** The `Content-Security-Policy` header is set in `application/routes/mod.rs`. If you add a new external script, stylesheet, font, or image source, update the corresponding CSP directive (`script-src`, `style-src`, `font-src`, `img-src`) or the browser will block it silently. Datastar requires `'unsafe-inline'` and `'unsafe-eval'` in `script-src`.
**10. Cookie `Secure` flag is on by default.** Session cookies are marked `Secure` unless `BREWLOG_INSECURE_COOKIES=true` is set. Local HTTP development needs this env var in `.env`. Do not use the old `BREWLOG_SECURE_COOKIES` variable — it no longer exists. **10. Cookie `Secure` flag is on by default.** Session cookies are marked `Secure` unless `BREWLOG_INSECURE_COOKIES=true` is set. Local HTTP development needs this env var in `.env`. Do not use the old `BREWLOG_SECURE_COOKIES` variable — it no longer exists.
**11. URL fields must validate scheme server-side.** Any user-supplied URL field (e.g., roaster `homepage`) must reject non-`http(s)` schemes to prevent `javascript:` or `data:` XSS. Use the `is_valid_url_scheme()` helper in `domain/roasters.rs` as a reference pattern. **11. URL fields must validate scheme server-side.** Any user-supplied URL field (e.g., roaster `homepage`) must reject non-`http(s)` schemes to prevent `javascript:` or `data:` XSS. Use the `is_valid_url_scheme()` helper in `domain/coffee/roasters.rs` as a reference pattern.
**12. Datastar create handlers must check referer for fragment targets.** When a `@post` creates an entity and returns a list fragment (e.g., `#brew-list`), that fragment only exists on the entity's data page. If the same `@post` can fire from other pages (homepage, timeline), check the `Referer` header and return a reload-script response for pages that lack the target element. See `create_brew` in `application/routes/api/brews.rs`. **12. Datastar create handlers must check referer for fragment targets.** When a `@post` creates an entity and returns a list fragment (e.g., `#brew-list`), that fragment only exists on the entity's data page. If the same `@post` can fire from other pages (homepage, timeline), check the `Referer` header and return a reload-script response for pages that lack the target element. See `create_brew` in `application/routes/api/coffee/brews.rs`.
## Backend Patterns ## Backend Patterns
@ -146,12 +161,12 @@ Social media preview cards are powered by Open Graph and Twitter Card meta tags
**Base URL** — `BREWLOG_RP_ORIGIN` is stored at startup via `set_base_url()` in `lib.rs` (a `OnceLock<String>`). Templates access it through the `base_url` field on their template struct, set with `crate::base_url()` in the handler. **Base URL** — `BREWLOG_RP_ORIGIN` is stored at startup via `set_base_url()` in `lib.rs` (a `OnceLock<String>`). Templates access it through the `base_url` field on their template struct, set with `crate::base_url()` in the handler.
**`og:image`** — a static 1200x630 PNG (`static/og-image.png`) served at `/og-image.png` via `include_bytes!()`. Only public shareable pages (home, bag detail, brew detail, cup detail, stats) include it via `{% block head %}`, since those are the pages social crawlers can access. **`og:image`** — a static 1200×630 PNG (`static/og-image.png`) served at `/static/og-image.png` via `include_bytes!()`. All detail pages (bag, brew, cafe, cup, gear, roast, roaster), plus home and stats, include it via `{% block head %}`.
**Adding OG tags to a new page:** **Adding OG tags to a new page:**
1. Add `pub base_url: &'static str` to the template struct 1. Add `pub base_url: &'static str` to the template struct
2. Set `base_url: crate::base_url()` in the handler 2. Set `base_url: crate::base_url()` in the handler
3. Override `{% block og_title %}`, `{% block og_description %}`, and add `<meta property="og:image" content="{{ base_url }}/og-image.png" />` in `{% block head %}` 3. Override `{% block og_title %}`, `{% block og_description %}`, and add `<meta property="og:image" content="{{ base_url }}/static/og-image.png" />` in `{% block head %}`
### Repository Pattern ### Repository Pattern
@ -188,7 +203,7 @@ if let Err(err) = self.timeline_repo.insert(entity.to_timeline_event()).await {
### Route Module Structure ### Route Module Structure
Each list-bearing route module (roasters, roasts, bags, gear, brews) follows the same structure: Each list-bearing route module (roasters, roasts, bags, brews, cups, cafes, gear) follows the same structure:
1. **Path constants**`ENTITY_PAGE_PATH` (full page URL) and `ENTITY_FRAGMENT_PATH` (with `#entity-list` anchor) 1. **Path constants**`ENTITY_PAGE_PATH` (full page URL) and `ENTITY_FRAGMENT_PATH` (with `#entity-list` anchor)
2. **`load_entity_page()`** — calls `repo.list()` and builds view models via `build_page_view()` from `support.rs` 2. **`load_entity_page()`** — calls `repo.list()` and builds view models via `build_page_view()` from `support.rs`
@ -209,17 +224,28 @@ if is_datastar_request(&headers) {
### Detail Pages ### Detail Pages
Three shareable detail pages (`/brews/:id`, `/cups/:id`, `/bags/:id`) share layout and logic via extracted template macros and Rust helpers. Seven detail pages share layout via extracted template macros and Rust helpers:
| Page | Route | Shared macros used |
|------|-------|--------------------|
| Bag | `/bags/{id}` | coffee_card, roaster_card, map_with_legend_2 |
| Brew | `/brews/{id}` | coffee_card, roaster_card, map_with_legend_2 |
| Cafe | `/cafes/{slug}` | map_with_legend_1 |
| Cup | `/cups/{id}` | coffee_card, roaster_card, map_with_legend_3 |
| Gear | `/gear/{id}` | (standalone layout) |
| Roast | `/roasters/{roaster_slug}/roasts/{roast_slug}` | coffee_card, roaster_card, map_with_legend_2 |
| Roaster | `/roasters/{slug}` | map_with_legend_1 |
**Template macros** — `templates/partials/detail_cards.html` provides: **Template macros** — `templates/partials/detail_cards.html` provides:
| Macro | Parameters | Used by | | Macro | Parameters | Used by |
|-------|-----------|---------| |-------|-----------|---------|
| `share_button()` | — | All detail pages | | `share_button()` | — | (defined, currently unused) |
| `share_script()` | — | All detail pages | | `share_script()` | — | (defined, currently unused) |
| `coffee_card(...)` | roast_name, roaster_name, origin, origin_flag, region, producer, process, tasting_notes | All detail pages | | `coffee_card(...)` | roast_name, roaster_name, origin, origin_flag, region, producer, process, tasting_notes | Bag, Brew, Cup, Roast |
| `roaster_card(...)` | name, country, country_flag, city, homepage | All detail pages | | `roaster_card(...)` | name, country, country_flag, city, homepage | Bag, Brew, Cup, Roast |
| `map_with_legend_2(...)` | map_countries, map_max, label1, opacity1, label2, opacity2 | Brew, Bag | | `map_with_legend_1(...)` | map_countries, map_max, label1, opacity1 | Cafe, Roaster |
| `map_with_legend_2(...)` | map_countries, map_max, label1, opacity1, label2, opacity2 | Bag, Brew, Roast |
| `map_with_legend_3(...)` | map_countries, map_max, label1-3, opacity1-3 | Cup | | `map_with_legend_3(...)` | map_countries, map_max, label1-3, opacity1-3 | Cup |
Detail page templates import with `{% import "partials/detail_cards.html" as detail %}` and call macros as `{{ detail::coffee_card(...) }}`. Detail page templates import with `{% import "partials/detail_cards.html" as detail %}` and call macros as `{{ detail::coffee_card(...) }}`.
@ -234,12 +260,14 @@ Detail page templates import with `{% import "partials/detail_cards.html" as det
Each `*DetailView::from_parts()` calls these helpers and flattens the results into its own struct (Askama needs direct field access). Each `*DetailView::from_parts()` calls these helpers and flattens the results into its own struct (Askama needs direct field access).
**Detail page layout** — all three follow the same grid structure: **Detail page layout** — pages using the shared macros (Bag, Brew, Cup, Roast) follow the same grid structure:
1. Header: page title + subtitle + share button 1. Header: page title + subtitle
2. Row 1 (2-col): Coffee card + Map with legend 2. Row 1 (2-col): Coffee card + Map with legend
3. Row 2 (2-col): Roaster card + page-specific card (Gear/Recipe for brew, Cafe for cup, Bag Info for bag) 3. Row 2 (2-col): Roaster card + page-specific card (Gear/Recipe for brew, Cafe for cup, Bag Info for bag)
4. Actions card (authenticated, full-width): page-specific buttons (e.g., Close Bag + Delete on bag page) 4. Actions card (authenticated, full-width): page-specific buttons (e.g., Close Bag + Delete on bag page)
Cafe, Gear, and Roaster detail pages have simpler standalone layouts.
**Route handlers** — each handler fetches the entity with related data (bag → roast → roaster), builds the `*DetailView`, and renders the template. Handlers live in `application/routes/app/{entity}.rs`. **Route handlers** — each handler fetches the entity with related data (bag → roast → roaster), builds the `*DetailView`, and renders the template. Handlers live in `application/routes/app/{entity}.rs`.
### Macros Reference ### Macros Reference
@ -469,6 +497,17 @@ Renders an inline SVG choropleth world map colored by country counts.
Uses `requestAnimationFrame` to defer rendering until after Datastar morphing completes. Uses `requestAnimationFrame` to defer rendering until after Datastar morphing completes.
**`<donut-chart>`** (`static/js/components/donut-chart.js`):
Renders an SVG donut chart with a legend, colored proportionally using the `--highlight-rgb` CSS custom property.
| Attribute | Purpose |
|-----------|---------|
| `data-items` | Pipe-separated `label:count` pairs (e.g. `V60:15\|Aeropress:8`) |
| `data-icon` | Icon key displayed in the center (`"beaker"` or `"grinder"`) |
Re-renders on theme change via `MutationObserver` on the `data-theme` attribute. Uses `requestAnimationFrame` to defer rendering.
### FlexiblePayload ### FlexiblePayload
Handlers accept both JSON and form data via `FlexiblePayload<T>`. When form fields don't map 1:1 to the domain `New*` struct (e.g., a roaster name needing resolution to an ID), use a `*Submission` newtype that handles the conversion. Handlers accept both JSON and form data via `FlexiblePayload<T>`. When form fields don't map 1:1 to the domain `New*` struct (e.g., a roaster name needing resolution to an ID), use a `*Submission` newtype that handles the conversion.

View file

@ -9,6 +9,11 @@ CLI client.
- **CLI client** for terminal-based workflows - **CLI client** for terminal-based workflows
- **AI extraction** — scan a coffee bag label or type a description to auto-fill forms (via [OpenRouter](https://openrouter.ai/)) - **AI extraction** — scan a coffee bag label or type a description to auto-fill forms (via [OpenRouter](https://openrouter.ai/))
- **Nearby cafe search** powered by [Foursquare Places](https://docs.foursquare.com/developer/reference/place-search) - **Nearby cafe search** powered by [Foursquare Places](https://docs.foursquare.com/developer/reference/place-search)
- **Cafe check-in** — log a cup of coffee at a cafe
- **Bag scanning** — photograph a coffee bag label to auto-create roaster, roast, and bag records
- **Shareable detail pages** for roasters, roasts, bags, brews, cups, cafes, and gear
- **Activity timeline** showing all logged events chronologically
- **Stats dashboard** with brewing, consumption, and geographic analytics
- **SQLite** database - **SQLite** database
- **Passkey authentication** — no passwords, WebAuthn only - **Passkey authentication** — no passwords, WebAuthn only