diff --git a/.gitignore b/.gitignore index 3aa9c2e..6dc64cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,21 @@ +# Build artifacts target/ result* + +# SQLite database files *.db *.db-journal *.db-shm *.db-wal -TODO.md -backup.json + +# Runtime configuration and secrets *.env -static/css/styles.css \ No newline at end of file + +# Generated by TailwindCSS CLI as part of `cargo build` +static/css/styles.css + +# This is generated by the nix shell hook +.pre-commit-config.yaml + +# Backup of database +backup.json diff --git a/CLAUDE.md b/CLAUDE.md index 7c39769..e70d790 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,11 +11,26 @@ Brewlog is a self-hosted coffee logging platform built in Rust. It provides: ## Build & Test Commands +Use `prek` to run the lints, tests and formatters all-in-one: + ```bash -cargo build # Build the project -cargo test # Run all tests +prek run -av +``` + +Individual checks can be run with `prek` if needed: + +```bash +prek run clippy -av +prek run cargo-test -av +``` + +Or the individual commands themselves: + +```bash +cargo build # Build the project +cargo test # Run all tests cargo clippy --allow-dirty --fix # Lint and auto-fix -cargo fmt # Format code +nix fmt # Format code ``` ### Database Migrations @@ -101,13 +116,15 @@ src/ These are non-obvious footguns that will cause bugs if missed. -**1. `datastar-fetch` event bubbles through the DOM.** When a page has multiple forms with `data-on:datastar-fetch` handlers, each handler fires for events from *any* `@post`/`@get` in the same DOM tree. **Every handler must guard with its own in-progress signal**: +**1. `datastar-fetch` event bubbles through the DOM.** When a page has multiple forms with `data-on:datastar-fetch` handlers, each handler fires for events from _any_ `@post`/`@get` in the same DOM tree. **Every handler must guard with its own in-progress signal**: ```html -
``` Only reset state on `finished` or `error`, never unconditionally. @@ -140,14 +157,14 @@ Only reset state on `finished` or `error`, never unconditionally. `infrastructure/database.rs` configures SQLite pragmas at connection time: -| Pragma | Value | Purpose | -|--------|-------|---------| -| `foreign_keys` | `ON` | Enforce FK constraints | -| `journal_mode` | `WAL` | Concurrent reads during writes | -| `synchronous` | `NORMAL` | Faster writes (safe with WAL) | -| `cache_size` | `-8000` | 8 MB page cache | -| `temp_store` | `MEMORY` | Temp tables in RAM | -| `busy_timeout` | `5000` | Wait up to 5s on lock contention | +| Pragma | Value | Purpose | +| -------------- | -------- | -------------------------------- | +| `foreign_keys` | `ON` | Enforce FK constraints | +| `journal_mode` | `WAL` | Concurrent reads during writes | +| `synchronous` | `NORMAL` | Faster writes (safe with WAL) | +| `cache_size` | `-8000` | 8 MB page cache | +| `temp_store` | `MEMORY` | Temp tables in RAM | +| `busy_timeout` | `5000` | Wait up to 5s on lock contention | When adding new pragmas, add them after the existing ones in `Database::connect()`. Connection pool is capped at 5 — appropriate for SQLite's single-writer model. @@ -164,6 +181,7 @@ Social media preview cards are powered by Open Graph and Twitter Card meta tags **`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:** + 1. Add `pub base_url: &'static str` to the template struct 2. Set `base_url: crate::base_url()` in the handler 3. Override `{% block og_title %}`, `{% block og_description %}`, and add `` in `{% block head %}` @@ -177,6 +195,7 @@ All data access goes through trait-based repositories defined in `domain/reposit Services in `application/services/` encapsulate "create entity + record timeline event" as a single operation. **When to use services vs repos:** + - **Services** — for `create()` (and `finish()` for bags). These record a timeline event after the insert. - **Repos** — for `get()`, `list()`, `update()`, `delete()`. No side effects needed. @@ -186,12 +205,12 @@ The `define_simple_service!` macro in `services/mod.rs` generates services for e Entities needing enrichment are hand-written: -| Service | Extra repos | Why | -|---------|-------------|-----| -| `RoastService` | `roaster_repo` | Needs roaster name/slug for timeline | -| `BagService` | `roast_repo`, `roaster_repo` | `create()` + `finish()`, needs roast+roaster for timeline | -| `BrewService` | — | `create()` enriches via `get_with_details()` for timeline + response | -| `CupService` | — | `create()` enriches via `get_with_details()` for timeline | +| Service | Extra repos | Why | +| -------------- | ---------------------------- | -------------------------------------------------------------------- | +| `RoastService` | `roaster_repo` | Needs roaster name/slug for timeline | +| `BagService` | `roast_repo`, `roaster_repo` | `create()` + `finish()`, needs roast+roaster for timeline | +| `BrewService` | — | `create()` enriches via `get_with_details()` for timeline + response | +| `CupService` | — | `create()` enriches via `get_with_details()` for timeline | Timeline events are display-only (not data integrity), so they use fire-and-forget error handling: @@ -226,41 +245,42 @@ if is_datastar_request(&headers) { 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 | +| 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: -| Macro | Parameters | Used by | -|-------|-----------|---------| -| `share_button()` | — | (defined, currently unused) | -| `share_script()` | — | (defined, currently unused) | -| `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 | Bag, Brew, Cup, Roast | -| `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 | +| Macro | Parameters | Used by | +| ------------------------ | --------------------------------------------------------------------------------------- | --------------------------- | +| `share_button()` | — | (defined, currently unused) | +| `share_script()` | — | (defined, currently unused) | +| `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 | Bag, Brew, Cup, Roast | +| `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 | Detail page templates import with `{% import "partials/detail_cards.html" as detail %}` and call macros as `{{ detail::coffee_card(...) }}`. **View model helpers** — `presentation/web/views/mod.rs` provides: -| Helper | Input | Purpose | -|--------|-------|---------| -| `build_coffee_info(roast)` | `&Roast` | Extracts origin, flag, region, producer, process, tasting notes | -| `build_roaster_info(roaster)` | `&Roaster` | Extracts country, flag, city, homepage | -| `build_map_data(entries)` | `&[(&str, u32)]` | Builds `data-countries` + `data-max` for `