Remove verbose reference tables, full template examples, and CSS class catalogs discoverable from code and doc comments. Retain all gotchas, workflow requirements, architecture overview, Datastar attribute table, three-way response pattern, and stats cache checklist.
14 KiB
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
prek run -av # All lints, tests, formatters
cargo build # Build
cargo test # Tests
cargo clippy --allow-dirty --fix # Lint + auto-fix
nix fmt # Format
sqlx migrate add <name> # New migration → migrations/NNNN_<name>.sql
Workflow Requirements
Before finishing any task, always:
- Run
prek run -av - Consider if the test coverage needs updating
- Update
README.mdif the change adds/removes/renames CLI commands, env vars, or user-facing features - Update
scripts/bootstrap-db.shif the change affects CLI commands, flags, or entity fields used by it - 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:
<form
data-on:submit="$_extracting = true; @post(...)"
data-on:datastar-fetch="if (!$_extracting) return;
if (evt.detail.type === 'finished') { $_extracting = false }
else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Failed.' }"
></form>
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 <section>, not nested inside it.
5. Table wrapper must be <section>, not <div> — <section class="rounded-lg border bg-surface">.
6. Infinite scroll sentinel needs md:hidden — <div class="infinite-scroll-sentinel h-4 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:
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:
- Add the field to the relevant domain struct (
RoastSummaryStats,ConsumptionStats,BrewingSummaryStats, orGeoStats) - Add the query in
SqlStatsRepository CachedStatsinherits the change via serde- 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
<brew-photo-capture>— camera/file picker, sets data URL ontarget-input, submitstarget-form<searchable-select>— filterable dropdown withname,placeholder,change/clearevents<chip-scroll>— horizontal scroll with chevron buttons, needs[data-chip-scroll],[data-scroll-left],[data-scroll-right]<world-map>— SVG choropleth viadata-countries(ISO:count pairs),data-max, optionaldata-selected<donut-chart>— SVG donut viadata-items(pipe-separated label:count),data-icon("beaker"/"grinder")
FlexiblePayload
Handlers accept JSON and form data via FlexiblePayload<T>. 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 <html>.
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 usehover:border-accent/40. - Typography: page title
text-3xl font-semibold, sectiontext-lg font-semibold, bodytext-sm text-text-secondary, mutedtext-xs text-text-muted. Font weights:boldfor stats only,semiboldfor headings/primary buttons,mediumfor 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). Alwaysshrink-0in flex. - Buttons: primary
bg-accent text-accent-text, outlinedborder text-sm font-medium, card actionh-8 border px-2 text-accent, linktext-sm font-medium text-accent. Submit buttons right-aligned. - Forms:
<label class="flex flex-col gap-1 text-sm">with.input-field. Required fields marked*. Multi-column:grid gap-4 sm:grid-cols-2. - Spacing: page sections
gap-8, form sectionsgap-6, field groupsgap-4, button groupsgap-2/gap-3, label-to-inputgap-1.
Tables & Lists
List partials in templates/partials/lists/. Table macros in table.html: search_header(), pagination_header(), sortable_header(). Every table has "Added" as first sortable column. Tables use .responsive-table (cards on mobile, table on desktop). Desktop: pagination controls. Mobile: infinite scroll via IntersectionObserver.
Code Style
Rust
- Extract closures >10 lines into named functions
- DRY 3+ similar blocks into helpers
- Prefer
matchoverif/else-ifon same variable - Extract shared predicates and generic helpers for repeated patterns
JavaScript
const/letonly, nevervar- Arrow functions only, never
functiondeclarations - Template literals for interpolation, never
+ - Inline
onclick+ global arrow functions, notDOMContentLoaded+addEventListener
Copy & UI Text
No second-person pronouns — never "you"/"your" in user-facing strings. Use imperative or impersonal phrasing.
Naming & Conventions
- Sort method:
order_clause()notsort_clause() - SQL: raw strings
r#"..."#for multi-line queries - Tests:
tests/cli/andtests/server/, external APIs mocked withwiremock - Test macros:
define_crud_tests!,define_datastar_entity_tests!,define_cli_auth_test!,define_cli_list_test!— see source files for usage - Commits: Conventional Commits, never add "Co-Authored-By" trailers, never use
--no-gpg-sign, never commit unless explicitly prompted