diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f643baf..47e81fa 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -67,10 +67,10 @@ jobs: cargo-${{ runner.os }}- - name: Install system dependencies - run: sudo apt-get update && sudo apt-get install -y clang lld libssl-dev pkg-config + run: sudo apt-get update && sudo apt-get install -y clang lld libssl-dev pkg-config libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64 libnss3 libxshmfence1 - - name: Install Chrome for E2E - uses: browser-actions/setup-chrome@v1 + - name: Install Chrome for Testing + run: mise run install-e2e - name: Run E2E tests run: cargo test --features e2e --test e2e -- --show-output diff --git a/.mise.toml b/.mise.toml index bcbb312..518e7cc 100644 --- a/.mise.toml +++ b/.mise.toml @@ -14,7 +14,14 @@ flyctl = "latest" SQLX_OFFLINE = "false" [tasks] -install-deps = "sudo apt-get update && sudo apt-get install -y --no-install-recommends clang lld libssl-dev pkg-config sqlite3" +install-deps = "sudo apt-get update && sudo apt-get install -y --no-install-recommends clang lld libssl-dev pkg-config sqlite3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64 libnss3 libxshmfence1" + +install-e2e = """ +CHROME_DIR="${MISE_DATA_DIR:-$HOME/.local/share/mise}/chrome-for-testing" +mkdir -p "$CHROME_DIR" +npx --yes @puppeteer/browsers install chrome@stable --path "$CHROME_DIR" +npx --yes @puppeteer/browsers install chromedriver@stable --path "$CHROME_DIR" +""" install-py-deps = "uv tool install djlint" diff --git a/README.md b/README.md index d11b836..7363a20 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,16 @@ -![cover image](./static/og-image.png) - -**B{rew}log** is a self-hosted specialty coffee logging platform optimised for filter brewing -enthusiasts. B{rew}log can be used for tracking roasters, roasts, brews, cafes and brewing gear. - -B{rew}log features an LLM-powered "Bag Scanning" feature, which enables it to automatically fill -roaster and coffee information using a photo of a bag. It also supports "check-ins" to log coffee -enjoyed in a cafe. - -B{rew}log ships as a single Rust binary that serves a web UI, a REST API, and a CLI client. The -application uses SQLite as a backend, and will automatically create and migrate the database on -start-up. - -> [!NOTE] -> This application was built almost entirely with Claude Code. I used this project as an -> excuse to explore the current state of the art in agentic coding tools. I've reviewed -> much of the code, and I'm largely responsible for the rules and the layout of the repository -> but nonetheless the vast majority of the code was written by Claude and Opus 4.6. - -## Quick Start (Demo) - -Before you start, you'll need to sign up for [Openrouter](https://openrouter.ai) and -[Foursquare Places](https://foursquare.com/products/places/) and get API keys for both. - -Then create a `docker.env` file: - -```env -# You'll need an API key from OpenRouter -BREWLOG_OPENROUTER_API_KEY=sk-or-... -# I've had good results with Gemini models, but you can try 'openrouter/free' to experiment -BREWLOG_OPENROUTER_MODEL=google/gemini-3-flash-preview -# FourSquare Places API key for location searching -BREWLOG_FOURSQUARE_API_KEY=fsq3... -``` - -You can see the full list of configuration options [below](#configuration). Once your `.env` file -is complete, start the container using the environment file - -```bash -# Create a data directory to store the database -mkdir data -# Run the container -docker run \ - --rm \ - -p 3000 \ - --env-file docker.env \ - -v $PWD/data:/data \ - ghcr.io/jnsgruk/brewlog:latest -``` - -On first start with an empty database the server prints a one-time registration URL: - -``` -No users found. Register the first user at: - http://localhost:3000/register/abc123... -This link expires in 1 hour. -``` - -Open that URL, choose a display name, and register a passkey. This creates an account and -signs in automatically. - -### Install from Git - -To build and install from source, you'll need a working Rust toolchain: - -```bash -cargo install --locked --git https://github.com/jnsgruk/brewlog.git -``` - -Then create a `.env` file containing at least your OpenRouter and Foursquare API keys, and start -the server: - -```bash -brewlog serve -``` - -### CLI Authentication - -To use the CLI or API for write operations, create a token via browser hand-off: - -```bash -brewlog token create --name "my-cli-token" -# Browser opens → authenticate with a passkey → token printed once - -export BREWLOG_URL="http://localhost:3000" -export BREWLOG_TOKEN="" - -# Create data from the CLI -brewlog roaster add --name "Radical Roasters" --country "United Kingdom" -``` - -Run `brewlog --help` for the full command reference. - -## Configuration - -All settings are read from environment variables or CLI flags. A `.env` file in the working -directory is loaded automatically via [dotenvy](https://crates.io/crates/dotenvy). - -### Server (`brewlog serve`) - -| Variable | Purpose | Default | -| -------------------------- | ---------------------------------------------------------------------- | ----------------------- | -| `BREWLOG_RP_ID` | WebAuthn Relying Party ID (server domain) | `localhost` | -| `BREWLOG_RP_ORIGIN` | WebAuthn Relying Party origin (full URL) | `http://localhost:3000` | -| `BREWLOG_DATABASE_URL` | Database connection string | `sqlite://brewlog.db` | -| `BREWLOG_BIND_ADDRESS` | Server bind address | `127.0.0.1:3000` | -| `BREWLOG_INSECURE_COOKIES` | Disable the `Secure` cookie flag (auto-enabled for localhost defaults) | `false` | -| `RUST_LOG` | Log level filter | `info` | -| `RUST_LOG_FORMAT` | Set to `json` for structured log output | — | - -### CLI Client - -| Variable | Purpose | Default | -| --------------- | ------------------------------------- | ----------------------- | -| `BREWLOG_URL` | Server URL | `http://localhost:3000` | -| `BREWLOG_TOKEN` | API bearer token for write operations | — | - -### Integrations - -| Variable | Purpose | Default | -| ---------------------------- | --------------------------------------------------------------------------- | ----------------- | -| `BREWLOG_OPENROUTER_API_KEY` | [OpenRouter](https://openrouter.ai/) API key for AI extraction | **required** | -| `BREWLOG_OPENROUTER_MODEL` | LLM model for AI extraction | `openrouter/free` | -| `BREWLOG_FOURSQUARE_API_KEY` | [Foursquare](https://foursquare.com/) Places API key for nearby cafe search | **required** | - ## Development Setup Install [mise](https://mise.jdx.dev/) then set up the development environment: ```bash sudo apt update -sudo apt install -y clang mold pkg-config libssl-dev +sudo apt install -y clang mold pkg-config libssl-dev \ + libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 \ + libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 \ + libcairo2 libasound2t64 libnss3 libxshmfence1 mise trust mise install # Install all dev tools -uvx djlint --version # Install djlint via uv (for template formatting) -prek install # Install git hooks -``` - -## Development Setup - -Install [mise](https://mise.jdx.dev/) then set up the development environment: - -```bash -sudo apt update -sudo apt install -y clang mold pkg-config libssl-dev -mise trust -mise install # Install all dev tools -uvx djlint --version # Install djlint via uv (for template formatting) +mise run install-e2e # Install Chrome for Testing + ChromeDriver prek install # Install git hooks ``` diff --git a/tests/e2e/helpers/browser.rs b/tests/e2e/helpers/browser.rs index e16a2fc..247c916 100644 --- a/tests/e2e/helpers/browser.rs +++ b/tests/e2e/helpers/browser.rs @@ -18,6 +18,7 @@ impl BrowserSession { caps.add_arg("--disable-gpu")?; caps.add_arg("--disable-dev-shm-usage")?; caps.add_arg("--window-size=1280,1024")?; + caps.set_binary(&find_chrome_binary())?; let driver = WebDriver::new(&chromedriver_url, caps).await?; driver @@ -40,3 +41,35 @@ impl BrowserSession { let _ = self.driver.quit().await; } } + +fn find_chrome_binary() -> String { + find_chrome_for_testing_binary("chrome", "chrome-linux64/chrome") +} + +fn find_chrome_for_testing_binary(subdir: &str, binary: &str) -> String { + let base = std::env::var("CHROME_FOR_TESTING_DIR").unwrap_or_else(|_| { + format!( + "{}/chrome-for-testing", + std::env::var("MISE_DATA_DIR").unwrap_or_else(|_| { + format!( + "{}/.local/share/mise", + std::env::var("HOME").unwrap_or_default() + ) + }) + ) + }); + + let base_path = std::path::Path::new(&base).join(subdir); + if let Ok(entries) = std::fs::read_dir(&base_path) { + for entry in entries.flatten() { + let path = entry.path().join(binary); + if path.exists() { + return path.to_string_lossy().to_string(); + } + } + } + + panic!( + "Chrome for Testing binary not found at {base_path:?}. Run `mise run install-e2e` to install it." + ); +} diff --git a/tests/e2e/helpers/chromedriver.rs b/tests/e2e/helpers/chromedriver.rs index 9b4a333..e307e9b 100644 --- a/tests/e2e/helpers/chromedriver.rs +++ b/tests/e2e/helpers/chromedriver.rs @@ -37,7 +37,7 @@ fn spawn_chromedriver(port: u16) -> Child { let (tx, rx) = std::sync::mpsc::sync_channel(1); std::thread::spawn(move || { - let mut cmd = Command::new("chromedriver"); + let mut cmd = Command::new(find_chromedriver_binary()); cmd.arg(format!("--port={port}")) .stdout(Stdio::null()) .stderr(Stdio::null()); @@ -110,3 +110,35 @@ pub fn ensure_chromedriver() -> u16 { guard.as_ref().unwrap().port } + +fn find_chromedriver_binary() -> String { + find_chrome_for_testing_binary("chromedriver", "chromedriver-linux64/chromedriver") +} + +fn find_chrome_for_testing_binary(subdir: &str, binary: &str) -> String { + let base = std::env::var("CHROME_FOR_TESTING_DIR").unwrap_or_else(|_| { + format!( + "{}/chrome-for-testing", + std::env::var("MISE_DATA_DIR").unwrap_or_else(|_| { + format!( + "{}/.local/share/mise", + std::env::var("HOME").unwrap_or_default() + ) + }) + ) + }); + + let base_path = std::path::Path::new(&base).join(subdir); + if let Ok(entries) = std::fs::read_dir(&base_path) { + for entry in entries.flatten() { + let path = entry.path().join(binary); + if path.exists() { + return path.to_string_lossy().to_string(); + } + } + } + + panic!( + "ChromeDriver binary not found at {base_path:?}. Run `mise run install-e2e` to install it." + ); +}