···11+# Binaries
22+bin/
33+tmp/
44+*.exe
55+*.exe~
66+*.dll
77+*.so
88+*.dylib
99+1010+# Test binary
1111+*.test
1212+1313+# Output of the go coverage tool
1414+*.out
1515+1616+# Database
1717+*.db
1818+*.db-shm
1919+*.db-wal
2020+2121+# Generated files
2222+*_templ.go
2323+web/static/css/output.css
2424+2525+# IDE
2626+.vscode/
2727+.idea/
2828+*.swp
2929+*.swo
3030+*~
3131+3232+# OS
3333+.DS_Store
3434+Thumbs.db
3535+3636+# Dependencies
3737+vendor/
3838+3939+# Logs
4040+*.log
4141+build-errors.log
4242+4343+# Nix
4444+result
4545+result-*
+187
README.md
···11+# Arabica - Coffee Brew Tracker
22+33+A self-hosted web application for tracking your coffee brewing journey. Built with Go, Templ, and SQLite.
44+55+## Features
66+77+- 📝 Quick entry of brew data (temperature, time, method, flexible grind size entry, etc.)
88+- ☕ Organize beans by origin and roaster with quick-select dropdowns
99+- 📱 Mobile-first PWA design for on-the-go tracking
1010+- 📊 Rating system and tasting notes
1111+- 📥 Export your data as JSON
1212+- 🔄 CRUD operations for all brew entries
1313+- 🗄️ SQLite database with abstraction layer for easy migration
1414+1515+## Tech Stack
1616+1717+- **Backend**: Go 1.22+ (using stdlib router)
1818+- **Database**: SQLite (via modernc.org/sqlite - pure Go, no CGO)
1919+- **Templates**: Templ (type-safe HTML templates)
2020+- **Frontend**: HTMX + Alpine.js
2121+- **CSS**: Tailwind CSS
2222+- **PWA**: Service Worker for offline support
2323+2424+## Project Structure
2525+2626+```
2727+arabica/
2828+├── cmd/server/ # Application entry point
2929+├── internal/
3030+│ ├── database/ # Database interface & SQLite implementation
3131+│ ├── models/ # Data models
3232+│ ├── handlers/ # HTTP handlers
3333+│ └── templates/ # Templ templates
3434+├── web/static/ # Static assets (CSS, JS, PWA files)
3535+├── migrations/ # Database migrations
3636+└── Makefile # Build commands
3737+```
3838+3939+## Getting Started
4040+4141+### Prerequisites
4242+4343+- Go 1.22+
4444+- Templ CLI
4545+- Tailwind CSS CLI
4646+- (Optional) Air for hot reload
4747+4848+Or use Nix:
4949+5050+```bash
5151+nix develop
5252+```
5353+5454+### Installation
5555+5656+1. Clone the repository:
5757+```bash
5858+cd arabica-site
5959+```
6060+6161+2. Install dependencies:
6262+```bash
6363+make install-deps
6464+```
6565+6666+3. Build the application:
6767+```bash
6868+make build
6969+```
7070+7171+4. Run the server:
7272+```bash
7373+make run
7474+```
7575+7676+The application will be available at `http://localhost:8080`
7777+7878+### Development
7979+8080+For hot reload during development:
8181+8282+```bash
8383+make dev
8484+```
8585+8686+This uses Air to automatically rebuild when you change Go files or templates.
8787+8888+### Building Assets
8989+9090+```bash
9191+# Generate templ files
9292+make templ
9393+9494+# Build Tailwind CSS
9595+make css
9696+9797+# Or build everything
9898+make build
9999+```
100100+101101+## Usage
102102+103103+### Adding a Brew
104104+105105+1. Navigate to "New Brew" from the home page
106106+2. Select a bean (or add a new one with the "+ New" button)
107107+ - When adding a new bean, provide a **Name** (required) like "Morning Blend" or "House Espresso"
108108+ - Optionally add Origin, Roast Level, and Description
109109+3. Select a roaster (or add a new one)
110110+4. Fill in brewing details:
111111+ - Method (Pour Over, French Press, etc.)
112112+ - Temperature (°C)
113113+ - Brew time (seconds)
114114+ - Grind size (free text - enter numbers like "18" or "3.5" for grinder settings, or descriptions like "Medium" or "Fine")
115115+ - Grinder (optional)
116116+ - Tasting notes
117117+ - Rating (1-10)
118118+5. Click "Save Brew"
119119+120120+### Viewing Brews
121121+122122+Navigate to the "Brews" page to see all your entries in a table format with:
123123+- Date
124124+- Bean details
125125+- Roaster
126126+- Method and parameters
127127+- Rating
128128+- Actions (View, Delete)
129129+130130+### Exporting Data
131131+132132+Click "Export JSON" on the brews page to download all your data as JSON.
133133+134134+## Configuration
135135+136136+Environment variables:
137137+138138+- `DB_PATH`: Path to SQLite database (default: `./arabica.db`)
139139+- `PORT`: Server port (default: `8080`)
140140+141141+## Database Abstraction
142142+143143+The application uses an interface-based approach for database operations, making it easy to swap SQLite for PostgreSQL or another database later. See `internal/database/store.go` for the interface definition.
144144+145145+## PWA Support
146146+147147+The application includes:
148148+- Web App Manifest for "Add to Home Screen"
149149+- Service Worker for offline caching
150150+- Mobile-optimized UI with large touch targets
151151+152152+## Future Enhancements (Not in MVP)
153153+154154+- Statistics and analytics page
155155+- CSV export
156156+- Multi-user support (database already has user_id column)
157157+- Search and filtering
158158+- Photo uploads for beans/brews
159159+- Brew recipes and sharing
160160+161161+## Development Notes
162162+163163+### Why These Choices?
164164+165165+- **Go**: Fast compilation, single binary deployment, excellent stdlib
166166+- **modernc.org/sqlite**: Pure Go SQLite (no CGO), easy cross-compilation
167167+- **Templ**: Type-safe templates, better than text/template for HTML
168168+- **HTMX**: Progressive enhancement without heavy JS framework
169169+- **Nix**: Reproducible builds across environments
170170+171171+### Database Schema
172172+173173+See `migrations/001_initial.sql` for the complete schema.
174174+175175+Key tables:
176176+- `users`: Future multi-user support
177177+- `beans`: Coffee bean information
178178+- `roasters`: Roaster information
179179+- `brews`: Individual brew records with all parameters
180180+181181+## License
182182+183183+MIT
184184+185185+## Contributing
186186+187187+This is a personal project, but suggestions and improvements are welcome!
+22
build.sh
···11+#!/usr/bin/env bash
22+set -e
33+44+echo "🔧 Building Arabica..."
55+66+# Generate templ files
77+echo "📝 Generating templates..."
88+templ generate
99+1010+# Build CSS
1111+echo "🎨 Building CSS..."
1212+tailwindcss -i web/static/css/style.css -o web/static/css/output.css --minify
1313+1414+# Build Go binary
1515+echo "🚀 Building Go application..."
1616+mkdir -p bin
1717+go build -o bin/arabica cmd/server/main.go
1818+1919+echo "✅ Build complete!"
2020+echo ""
2121+echo "Run './bin/arabica' to start the server"
2222+echo "Or run 'make dev' for hot reload development mode"
+58
cmd/server/main.go
···11+package main
22+33+import (
44+ "log"
55+ "net/http"
66+ "os"
77+88+ "arabica/internal/database/sqlite"
99+ "arabica/internal/handlers"
1010+)
1111+1212+func main() {
1313+ // Get database path from env or use default
1414+ dbPath := os.Getenv("DB_PATH")
1515+ if dbPath == "" {
1616+ dbPath = "./arabica.db"
1717+ }
1818+1919+ // Initialize database
2020+ store, err := sqlite.NewSQLiteStore(dbPath)
2121+ if err != nil {
2222+ log.Fatalf("Failed to initialize database: %v", err)
2323+ }
2424+ defer store.Close()
2525+2626+ // Initialize handlers
2727+ h := handlers.NewHandler(store)
2828+2929+ // Create router
3030+ mux := http.NewServeMux()
3131+3232+ // Page routes (must come before static files)
3333+ mux.HandleFunc("GET /{$}", h.HandleHome) // {$} means exact match
3434+ mux.HandleFunc("GET /brews", h.HandleBrewList)
3535+ mux.HandleFunc("GET /brews/new", h.HandleBrewNew)
3636+ mux.HandleFunc("POST /brews", h.HandleBrewCreate)
3737+ mux.HandleFunc("DELETE /brews/{id}", h.HandleBrewDelete)
3838+ mux.HandleFunc("GET /brews/export", h.HandleBrewExport)
3939+4040+ // API routes for adding beans/roasters via AJAX
4141+ mux.HandleFunc("POST /api/beans", h.HandleBeanCreate)
4242+ mux.HandleFunc("POST /api/roasters", h.HandleRoasterCreate)
4343+4444+ // Static files (must come after specific routes)
4545+ fs := http.FileServer(http.Dir("web/static"))
4646+ mux.Handle("GET /static/", http.StripPrefix("/static/", fs))
4747+4848+ // Get port from env or use default
4949+ port := os.Getenv("PORT")
5050+ if port == "" {
5151+ port = "8080"
5252+ }
5353+5454+ log.Printf("Starting Arabica server on http://localhost:%s", port)
5555+ if err := http.ListenAndServe(":"+port, mux); err != nil {
5656+ log.Fatalf("Server failed to start: %v", err)
5757+ }
5858+}
···11+-- Initial schema for Arabica coffee tracking
22+33+CREATE TABLE IF NOT EXISTS users (
44+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55+ username TEXT UNIQUE NOT NULL,
66+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
77+);
88+99+CREATE TABLE IF NOT EXISTS beans (
1010+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1111+ name TEXT,
1212+ origin TEXT NOT NULL,
1313+ roast_level TEXT,
1414+ description TEXT,
1515+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
1616+);
1717+1818+CREATE TABLE IF NOT EXISTS roasters (
1919+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2020+ name TEXT UNIQUE NOT NULL,
2121+ location TEXT,
2222+ website TEXT,
2323+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2424+);
2525+2626+CREATE TABLE IF NOT EXISTS grinders (
2727+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2828+ name TEXT UNIQUE NOT NULL,
2929+ type TEXT,
3030+ notes TEXT,
3131+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
3232+);
3333+3434+CREATE TABLE IF NOT EXISTS brews (
3535+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3636+ user_id INTEGER NOT NULL DEFAULT 1,
3737+ bean_id INTEGER NOT NULL,
3838+ roaster_id INTEGER NOT NULL,
3939+ method TEXT NOT NULL,
4040+ temperature REAL,
4141+ time_seconds INTEGER,
4242+ grind_size TEXT,
4343+ grinder TEXT,
4444+ tasting_notes TEXT,
4545+ rating INTEGER CHECK(rating >= 1 AND rating <= 10),
4646+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
4747+ FOREIGN KEY (user_id) REFERENCES users(id),
4848+ FOREIGN KEY (bean_id) REFERENCES beans(id),
4949+ FOREIGN KEY (roaster_id) REFERENCES roasters(id)
5050+);
5151+5252+-- Insert default user for single-user mode (ignore if exists)
5353+INSERT OR IGNORE INTO users (username) VALUES ('default');
5454+5555+-- Create indexes for common queries
5656+CREATE INDEX IF NOT EXISTS idx_brews_user_id ON brews(user_id);
5757+CREATE INDEX IF NOT EXISTS idx_brews_bean_id ON brews(bean_id);
5858+CREATE INDEX IF NOT EXISTS idx_brews_roaster_id ON brews(roaster_id);
5959+CREATE INDEX IF NOT EXISTS idx_brews_created_at ON brews(created_at DESC);