# Claude Code Guidelines for Brewlog ## Project Overview Brewlog is a self-hosted coffee logging platform built in Rust: HTTP server (Axum + Askama + Datastar), REST API, CLI client, SQLite database. ## Build & Test Commands ```bash prek run -av # All lints, tests, formatters cargo build # Build cargo test # Tests cargo clippy --allow-dirty --fix # Lint + auto-fix mise run fmt # Format sqlx migrate add # New migration → migrations/NNNN_.sql ``` ## Workflow Requirements **Before finishing any task**, always: 1. Run `prek run -av` 1. Consider if the test coverage needs updating 1. Update `README.md` if the change adds/removes/renames CLI commands, env vars, or user-facing features 1. Update `scripts/bootstrap-db.sh` if the change affects CLI commands, flags, or entity fields used by it 1. Provide a **draft commit message** using Conventional Commits format ## Architecture Clean Architecture / DDD with four layers: ``` src/ ├── domain/ # Pure business logic, no external deps │ ├── errors.rs # RepositoryError enum │ ├── ids.rs # Typed ID wrappers (RoasterId, BagId, BrewId, etc.) │ ├── listing.rs # Pagination & sorting (SortKey, ListRequest, Page, PageSize) │ ├── repositories.rs # Repository traits │ ├── countries.rs # Country name → ISO code, flag emoji │ ├── formatting.rs # format_relative_time(), format_weight() │ ├── coffee/ # roasters, roasts, bags, brews, cups, gear, cafes │ ├── auth/ # users, sessions, tokens, passkeys, registration_tokens │ └── analytics/ # timeline, stats, country_stats, ai_usage ├── infrastructure/ # DB, HTTP clients, third-party APIs │ ├── repositories/ # SQL impls of repository traits (coffee/, auth/, analytics/) │ ├── client/ # HTTP client for CLI │ ├── ai.rs # OpenRouter LLM integration │ ├── foursquare.rs # Foursquare Places API │ ├── backup.rs # Database backup/restore │ └── database.rs # Database pool + SQLite pragmas ├── application/ # HTTP server, routes, middleware, services │ ├── routes/ # Axum handlers (api/ for REST, app/ for web UI) │ ├── services/ # Entity services (create + timeline event) │ └── errors.rs # HTTP error mapping └── presentation/ # User interfaces ├── cli/ # CLI commands └── web/ # View models for templates ``` **Dependency flow**: `presentation → application → domain ← infrastructure` ## Gotchas These are non-obvious footguns that will cause bugs if missed. **1. `datastar-fetch` event bubbles through the DOM.** Every `data-on:datastar-fetch` handler must guard with its own in-progress signal, or it fires for events from _any_ `@post`/`@get` in the DOM tree: ```html
``` **2. No `data-model` in Datastar v1** — silently ignored. Use `data-bind:_signal-name`. **3. Signal patching requires JSON, not HTML.** Use `render_signals_json()`, not `data-signals` in DOM fragments. **4. List partial must be OUTSIDE the form section** — sibling of the form `
`, not nested inside it. **5. Table wrapper must be `
`, not `
`** — `
`. **6. Infinite scroll sentinel needs `md:hidden`** — `
`. **7. Use token-based text classes, never `text-stone-*`.** Use `text-text`, `text-text-secondary`, `text-text-muted`. **8. Static assets need explicit routes and cache headers.** Embedded via `include_str!()`/`include_bytes!()` with explicit routes in `application/routes/app/mod.rs`. All under `/static/`. Every handler must return `cache-control: public, max-age=604800`. **9. CSP must be updated when adding external resources.** Set in `application/routes/mod.rs`. Datastar requires `'unsafe-inline'` and `'unsafe-eval'` in `script-src`. **10. Cookie `Secure` flag is on by default.** Set `BREWLOG_INSECURE_COOKIES=true` for local HTTP dev. **11. URL fields must validate scheme server-side.** Reject non-`http(s)` schemes to prevent XSS. See `is_valid_url_scheme()` in `domain/coffee/roasters.rs`. **12. Datastar create handlers must check referer for fragment targets.** If a `@post` can fire from pages lacking the target element, check `Referer` and return a reload-script. See `create_brew` in `application/routes/api/coffee/brews.rs`. ## Backend Patterns ### Repository Pattern Repositories defined as traits in `domain/repositories.rs`, SQL impls in `infrastructure/repositories/`. Each uses a private `Record` struct with `to_domain()`. Use typed ID wrappers from `domain/ids.rs` — never raw `i64`. ### Service Layer Services (`application/services/`) encapsulate "create + timeline event". Use **services** for `create()` (and `finish()` for bags), **repos** for `get()`/`list()`/`update()`/`delete()`. `define_simple_service!` macro generates services for `RoasterService`, `CafeService`, `GearService`. Others (`RoastService`, `BagService`, `BrewService`, `CupService`) are hand-written because they need enrichment from related repos. Timeline events use fire-and-forget: `if let Err(err) = ... { warn!(...) }`. ### Route Module Structure Each list-bearing route follows: path constants → `load_entity_page()` → `entity_page()` (fragment vs full page via `is_datastar_request()`) → `render_entity_list_fragment()`. Create handlers use a three-way response pattern: ```rust if is_datastar_request(&headers) { render_fragment(...) // Datastar → updated list fragment } else if matches!(source, PayloadSource::Form) { Redirect::to(...) // Browser form → redirect } else { Json(entity) // API → JSON } ``` ### Detail Pages Seven detail pages (bag, brew, cafe, cup, gear, roast, roaster) share macros from `templates/partials/detail_cards.html` and helpers from `presentation/web/views/mod.rs` (`build_coffee_info`, `build_roaster_info`, `build_map_data`). ### Macros All macros have doc comments. Key ones: `define_simple_service!`, `define_get_handler!`, `define_enriched_get_handler!`, `define_delete_handler!`, `define_list_fragment_renderer!`, `define_get_command!`, `define_delete_command!`, `push_update_field!`. Check source files for usage. ### SQL & Queries Use `QueryBuilder` for dynamic queries, `push_update_field!` for UPDATEs. Sort method is `order_clause()` (not `sort_clause`). ### Stats Cache Stats are pre-computed in `stats_cache` via a background task with 2-second debouncing (`application/services/stats.rs`). **Every entity create/update/delete handler must call `state.stats_invalidator.invalidate()`** — the `define_delete_handler!` macro does this automatically. **Adding new stats:** 1. Add the field to the relevant domain struct (`RoastSummaryStats`, `ConsumptionStats`, `BrewingSummaryStats`, or `GeoStats`) 2. Add the query in `SqlStatsRepository` 3. `CachedStats` inherits the change via serde 4. The stats page template can reference the new field immediately ### Error Handling Error types: `RepositoryError` (domain), `AppError` (HTTP), `anyhow::Result` (CLI). Never silently discard errors — log before `map_err`, avoid bare `.ok()`, use `if let Err` instead of `let _ =`. Every create/update/delete logs at `info!` with entity ID. ### Open Graph Base URL from `BREWLOG_RP_ORIGIN` via `crate::base_url()`. To add OG tags: add `pub base_url: &'static str` to template struct, override `{% block og_title %}`, `{% block og_description %}`, add og:image in `{% block head %}`. ## Datastar & Frontend ### Core Concepts Key Datastar attributes: | Attribute | Purpose | | ---------------------------- | ---------------------------------------- | | `data-signals:_name="value"` | Declare local signal (underscore prefix) | | `data-show="$_signal"` | Conditional visibility | | `data-bind:_signal-name` | Two-way binding to input | | `data-on:event="expr"` | Event handler | | `data-text="$_signal"` | Set text content from signal | | `data-attr:attr="$_signal"` | Set attribute from signal | | `@get/@post/@put/@delete` | HTTP actions with Datastar headers | Signal names: **kebab-case** in HTML (`data-signals:_roaster-name`), **camelCase** in JS (`$_roasterName`) and JSON (`_roasterName`). Two response formats: **HTML fragments** via `render_fragment(template, selector)`, **JSON signal patches** via `render_signals_json(&[("_signal-name", value)])` (pass kebab-case, auto-converts to camelCase). ### Datastar vs JavaScript **Datastar**: visibility toggling, list CRUD, debounced search, AI extraction, multi-step wizards, searchable selects. **JavaScript**: browser APIs (WebAuthn, clipboard, geolocation), infinite scroll, theme toggle, flows needing `window.location.reload()`. ### AI Extraction Pattern Extraction forms use `@post` with `data-on:datastar-fetch` guarded by `_extracting` signal. Server returns `render_signals_json()` which Datastar merges into form fields via `data-bind`. See existing extraction forms for the template pattern. ### Web Components - **``** — camera/file picker, sets data URL on `target-input`, submits `target-form` - **``** — filterable dropdown with `name`, `placeholder`, `change`/`clear` events - **``** — horizontal scroll with chevron buttons, needs `[data-chip-scroll]`, `[data-scroll-left]`, `[data-scroll-right]` - **``** — SVG choropleth via `data-countries` (ISO:count pairs), `data-max`, optional `data-selected` - **``** — SVG donut via `data-items` (pipe-separated label:count), `data-icon` ("beaker"/"grinder") ### FlexiblePayload Handlers accept JSON and form data via `FlexiblePayload`. Use `*Submission` newtypes when form fields don't map 1:1 to domain structs. ## Design System ### CSS Build Tailwind CSS v4 via standalone CLI. `build.rs` runs it during `cargo build`. Source: `static/css/input.css`. Output: `static/css/styles.css` (gitignored). ### Design Tokens Defined in `input.css` (`:root` light, `[data-theme="dark"]` dark). Key tokens: `bg-page`, `bg-surface`, `bg-surface-alt`, `bg-accent`, `text-text`, `text-text-secondary`, `text-text-muted`, `text-accent`, `text-accent-text`. Dark mode via `[data-theme="dark"]` on ``. ### Component Classes Defined in `input.css`: `.input-field`, `.btn-adjust`, `.sticky-submit`, `.pill` + variants (`.pill-muted`, `.pill-success`, `.pill-warning`, `.pill-floral` through `.pill-vegetal`), `.tab`/`.tab-active`, `.tab-mobile`/`.tab-mobile-active`, `.responsive-table`, `.scrollbar-hide`, `.timeline-*`/`.tl-card`, `.text-2xs`, `.small-caps`. ### Key UI Rules - Cards: `rounded-lg border bg-surface` — no shadows. Clickable cards use `hover:border-accent/40`. - Typography: page title `text-3xl font-semibold`, section `text-lg font-semibold`, body `text-sm text-text-secondary`, muted `text-xs text-text-muted`. Font weights: `bold` for stats only, `semibold` for headings/primary buttons, `medium` for secondary actions. - Icons: entity mapping — brew=beaker, roast=coffee_bean, roaster=fire, bag=bag, cup=cup, cafe=location, gear=grinder. Sizes: `h-3 w-3` (inline labels), `h-4 w-4` (buttons), `h-5 w-5` (nav/spinners), `h-6 w-6` (stat cards). Always `shrink-0` in flex. - Buttons: primary `bg-accent text-accent-text`, outlined `border text-sm font-medium`, card action `h-8 border px-2 text-accent`, link `text-sm font-medium text-accent`. Submit buttons right-aligned. - Forms: `