chore: comprehensive README with binary distribution docs + build-all.sh

This commit is contained in:
Claus Lohmar 2026-05-29 19:54:20 +00:00
parent ca970104ee
commit 10c22bcaee
3 changed files with 341 additions and 176 deletions

4
.gitignore vendored
View file

@ -24,5 +24,9 @@ storage/*
.DS_Store
Thumbs.db
# Build output
dist/
expenseflow-*
# Go
vendor/

463
README.md
View file

@ -1,236 +1,288 @@
# ExpenseFlow - AI-Powered Expense Tracker
# ExpenseFlow AI-Powered Expense Tracker
A production-ready, mobile-first Progressive Web App (PWA) for expense management with passwordless email OTP authentication, event-based expense tracking, AI receipt extraction via DeepSeek Vision, and CSV/PDF reporting delivered via email.
> A production-ready, mobile-first Progressive Web App (PWA) that uses passwordless email OTP login, event-based expense tracking, AI receipt extraction (DeepSeek Vision), and event filing (CSV/PDF via email).
**Tech Stack:** Go 1.22+ · HTMX · SQLite · DeepSeek Vision API · PWA
---
## Features
## 📦 Quick Start — Binary Distribution
- **Passwordless OTP Auth** — Email-based 6-digit code, no passwords, HTTP-only session cookie
- **Event Dashboard** — Create events (e.g. "WebSummit 2026"), track status (open/closed), reopen closed events
- **AI Receipt Capture** — Snap a photo → DeepSeek Vision extracts amount, merchant, category, date → save
- **Event Filing** — Generate CSV or PDF report → email as attachment → auto-close event
- **PWA** — Installable on mobile homescreen, offline shell, camera capture for receipts
Pre-compiled binaries are available for download from the [Releases](https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases) page.
### Download & Run
```bash
# Linux (amd64)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-amd64
chmod +x expenseflow
./expenseflow
# Linux (arm64) — Raspberry Pi, etc.
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-linux-arm64
chmod +x expenseflow
./expenseflow
# macOS (Intel)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-amd64
chmod +x expenseflow
./expenseflow
# macOS (Apple Silicon M1/M2/M3)
curl -L -o expenseflow https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases/download/v1.0.0/expenseflow-darwin-arm64
chmod +x expenseflow
./expenseflow
```
The server starts on `http://localhost:8080` by default. Set `PORT=3000` to change the port.
> **Note:** The binary embeds no configuration. You must create a `.env` file (see [Configuration](#-configuration) below) in the same directory you run the binary from.
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| **Backend** | Go (`net/http` + chi router) |
| **Frontend** | HTMX (server-driven UI, partial page updates) |
| **Database** | SQLite (auto-migrated on startup) |
| **AI OCR** | DeepSeek Vision API |
| **Email** | SMTP (OTP delivery + report attachments) |
| **PWA** | `manifest.json` + Service Worker |
---
## Quick Start
## 🔧 Building from Source
### Prerequisites
- Go 1.21+
- GCC (required for CGO/SQLite via `mattn/go-sqlite3`)
- **Go 1.22+** — [Download](https://go.dev/dl/)
- **GCC** (CGO is required for the SQLite driver)
### Setup
```bash
# Debian/Ubuntu
sudo apt install build-essential
# macOS
xcode-select --install
# Alpine
apk add build-base
```
### Clone & Build
```bash
# Clone the repository
git clone https://github.com/your-org/expenseflow.git
cd expenseflow
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
cd ExpenseFlow
# Copy environment configuration
# Build for your current platform
go build -o expenseflow .
# The binary is now ready: ./expenseflow
```
### Cross-Compilation
The binary uses CGO (for `mattn/go-sqlite3`), so cross-compilation requires a cross-compiler.
```bash
# Linux amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc go build -o expenseflow-linux-amd64 .
# Linux arm64 (Raspberry Pi, etc.)
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -o expenseflow-linux-arm64 .
# macOS Intel
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 CC=o64-clang go build -o expenseflow-darwin-amd64 .
# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-apple-darwin-clang go build -o expenseflow-darwin-arm64 .
```
> **Tip:** For macOS cross-compilation from Linux, use [osxcross](https://github.com/tpoechtrager/osxcross). For ARM Linux, install `gcc-aarch64-linux-gnu`.
---
## ⚙️ Configuration
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cp .env.example .env
# Run the application
go run main.go
```
Open [http://localhost:8080](http://localhost:8080) in your browser.
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `SMTP_HOST` | Yes* | — | SMTP server hostname |
| `SMTP_PORT` | Yes* | — | SMTP server port (usually 587) |
| `SMTP_USER` | Yes* | — | SMTP username |
| `SMTP_PASS` | Yes* | — | SMTP password |
| `DEEPSEEK_API_KEY` | Yes* | — | DeepSeek Vision API key |
| `BASE_URL` | No | `http://localhost:8080` | Public URL for email links |
| `PORT` | No | `8080` | HTTP server port |
*\* The app will start without SMTP/DeepSeek configured, but OTP emails and AI extraction will not work.*
### Getting Credentials
- **DeepSeek Vision API:** Sign up at [platform.deepseek.com](https://platform.deepseek.com) and create an API key.
- **SMTP:** Use any SMTP provider. The default config points to an OX hosting SMTP server.
---
## Environment Variables
## 🚀 Usage
Copy `.env.example` to `.env` and configure:
### 1. Start the Server
| Variable | Description | Default |
|----------|-------------|---------|
| `SMTP_HOST` | SMTP server hostname | `smtp.openxchange.eu` |
| `SMTP_PORT` | SMTP server port | `587` |
| `SMTP_USER` | SMTP authentication username | `post@2-4-h.app` |
| `SMTP_PASS` | SMTP authentication password | — |
| `DEEPSEEK_API_KEY` | DeepSeek Vision API key | — |
| `BASE_URL` | Public base URL for absolute links in emails | `http://localhost:8080` |
```bash
./expenseflow
```
---
### 2. Open in Browser
## Project Structure
Navigate to [http://localhost:8080](http://localhost:8080)
### 3. Full Acceptance Flow
```
ExpenseFlow/
├── main.go # Entry point, router, static file server
├── go.mod # Module dependencies
├── .env # Credentials (not committed)
├── .env.example # Env template
├── internal/
│ ├── ai/
│ │ └── deepseek.go # DeepSeek Vision API client
│ ├── auth/
│ │ ├── otp.go # OTP generation & validation
│ │ └── session.go # Session management (in-memory store)
│ ├── database/
│ │ └── db.go # SQLite init, queries, auto-migration
│ ├── email/
│ │ └── smtp.go # SMTP email sender (OTP + reports)
│ ├── handlers/
│ │ ├── auth.go # Auth endpoints (login, OTP, verify)
│ │ ├── events.go # Event CRUD + reopen
│ │ ├── expenses.go # Upload, AI extract, save
│ │ └── file.go # CSV/PDF generation, email delivery
│ └── utils/
│ └── uuid.go # UUID generation helper
├── static/
│ ├── css/
│ │ └── style.css # Application styles
│ ├── icons/ # PWA icons (192x192, 512x512)
│ ├── manifest.json # PWA manifest
│ └── sw.js # Service Worker (offline cache)
├── templates/ # HTMX templates (server-rendered HTML)
│ ├── layout.html
│ ├── index.html
│ ├── dashboard.html
│ ├── event_expenses.html
│ ├── receipt_form.html
│ └── expense_list.html
└── storage/ # Uploaded receipt images (created at runtime)
1. Enter your email → click "Send Verification Code"
2. Check your inbox for the 6-digit OTP code
3. Enter the OTP → click "Verify Code"
4. Create an event (e.g., "WebSummit 2026")
5. Click "Add Expenses" on the event card
6. Click "Capture Receipt" → take a photo or select an image
7. AI extracts: amount, merchant, category, date → pre-fills the form
8. Review and click "Save Expense"
9. Click "File Event" → enter recipient email → choose CSV or PDF
10. Report is emailed and event status changes to "closed"
11. Click "Reopen" to re-open a closed event
```
---
## API Endpoints
## 📡 API Reference
### Public Endpoints (no authentication)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/` | Landing page — email input for OTP login |
| `POST` | `/request-otp` | Request a 6-digit OTP code (sent via email) |
| `POST` | `/verify-otp` | Verify OTP and create session cookie |
| `GET` | `/dashboard` | User dashboard — list of events |
| `POST` | `/events` | Create a new expense event |
| `GET` | `/events/{id}/expenses` | View expenses for an event |
| `POST` | `/expenses/upload` | Upload receipt image → AI extraction |
| `POST` | `/expenses` | Save an expense record |
| `POST` | `/events/{id}/file` | File report (CSV/PDF) and close event |
| `GET` | `/` | Landing page with email login form |
| `POST` | `/request-otp` | Request a 6-digit OTP code (sends email) |
| `POST` | `/verify-otp` | Verify OTP code and create session |
| `POST` | `/logout` | Clear session and redirect to login |
### Protected Endpoints (require session cookie)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/dashboard` | Event dashboard |
| `POST` | `/events` | Create a new event |
| `GET` | `/events/{id}/expenses` | View event with expense list |
| `PUT` | `/events/{id}/reopen` | Reopen a closed event |
| `POST` | `/expenses/upload` | Upload receipt image (multipart) |
| `POST` | `/expenses` | Save expense from form data |
| `POST` | `/events/{id}/file` | Generate report (CSV/PDF) and email it |
### Static Files
| Path | Description |
|------|-------------|
| `/static/css/style.css` | Application stylesheet |
| `/static/icons/icon-192.png` | PWA icon (192×192) |
| `/static/icons/icon-512.png` | PWA icon (512×512) |
| `/manifest.json` | PWA manifest |
| `/sw.js` | Service worker |
| `/storage/{filename}` | Uploaded receipt images |
---
## Feature Walkthrough
## 🏗️ Project Structure
### 1. Passwordless OTP Auth
Log in with just your email — no passwords needed.
```mermaid
sequenceDiagram
User->>Browser: Enter email
Browser->>Server: POST /request-otp (email)
Server->>Email: Send 6-digit code
User->>Browser: Enter code
Browser->>Server: POST /verify-otp (email, code)
Server->>Browser: Set session cookie, redirect to /dashboard
```
### 2. Event Dashboard
- **Open events** — click to view expenses and capture receipts
- **Closed events** — show a **Reopen** button (`PUT /events/{id}/reopen`)
- Each card shows: event name, status badge, created date
### 3. AI Receipt Capture
```bash
# Upload a receipt image
curl -X POST http://localhost:8080/expenses/upload \
-H "Cookie: session_token=..." \
-F "image=@receipt.jpg"
ExpenseFlow/
├── main.go # Entry point, router, middleware, server
├── go.mod / go.sum # Go module definition
├── .env.example # Environment variable template
├── README.md # This file
├── internal/
│ ├── database/db.go # SQLite init, auto-migration, 11 query functions
│ ├── auth/
│ │ ├── otp.go # 6-digit OTP generation + 3-fail lockout
│ │ └── session.go # In-memory session store (crypto tokens, 24h TTL)
│ ├── handlers/
│ │ ├── auth.go # Auth endpoints + middleware
│ │ ├── events.go # Event CRUD + dashboard
│ │ ├── expenses.go # Receipt upload, AI extraction, save
│ │ └── file.go # CSV/PDF generation + email filing
│ ├── ai/deepseek.go # DeepSeek Vision API client
│ ├── email/smtp.go # SMTP sender (OTP + attachments)
│ └── utils/uuid.go # UUID generation
├── templates/
│ ├── index.html # Landing page
│ ├── dashboard.html # Event cards + create form
│ ├── event_expenses.html # Event detail + capture + filing
│ ├── receipt_form.html # AI-prefilled edit form
│ └── expense_list.html # HTMX expense list fragment
├── static/
│ ├── css/style.css # Mobile-first responsive CSS (1845 lines)
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service worker
│ └── icons/ # PWA placeholder icons
└── storage/ # Uploaded receipts (created at runtime)
```
What happens:
1. Image saved to `./storage/{uuid}.jpg`
2. Sent to DeepSeek Vision API for OCR
3. AI returns: `{amount, currency, merchant, category, date}`
4. Pre-filled form rendered for user review/edit
5. On save → expense stored, list updated
### 4. Event Filing
Generate a CSV or PDF report and email it:
```bash
# File event as PDF
curl -X POST http://localhost:8080/events/123/file \
-H "Cookie: session_token=..." \
-d "email=user@example.com&format=pdf"
```
- Creates report from all event expenses
- Sends email with attachment
- Event status changes to `closed`
---
## Acceptance Test Scenario
## 🧩 Features in Detail
Run through the full flow manually:
### 🔐 Passwordless OTP Authentication
- Email-based 6-digit code, 5-minute expiry
- Auto-creates user account on first login
- 3 failed attempts trigger a 1-minute cooldown
- HTTP-only session cookie (`SameSite=Lax`, 24h TTL)
- No passwords to store or forget
1. **Visit** `http://localhost:8080` — see email input
2. **Enter email** — receive 6-digit OTP in inbox
3. **Enter OTP** — redirected to dashboard (empty)
4. **Create event** — name it "Test Event"
5. **Click event** — see expense list (empty) + **Capture Receipt** button
6. **Upload receipt** — snap/take a photo → AI extracts amount, merchant, category, date
7. **Review & save** — pre-filled form, click Save → expense appears in list
8. **File event** — click **File Event**, enter email, pick PDF → report sent, event closes
9. **Verify** — event shows as **closed** on dashboard
10. **Reopen** — click **Reopen** → badge changes back to **open**
### 📋 Event-Based Expense Tracking
- Group expenses into events (trips, conferences, months)
- Open/closed lifecycle with reopen support
- Dashboard with event cards showing name, status, and creation date
### 🤖 AI Receipt Extraction
- Upload receipt images (JPEG/PNG, max 10 MB)
- DeepSeek Vision API extracts: amount, currency, merchant, category, date
- Editable pre-filled form on failure or success
- Images stored locally in `./storage/`
### 📧 Email Reporting
- Generate CSV (via `encoding/csv`) or PDF (via `gofpdf`)
- Automatic email delivery via SMTP with file attachment
- Event auto-closes after successful filing
- Multipart MIME support with proper content headers
### 📱 Progressive Web App
- Installable on mobile and desktop (manifest.json)
- Offline shell caching (service worker)
- Camera capture for receipts (`capture="environment"`)
- Theme color: `#10b981` (emerald green)
- Responsive design: 320px → 768px → 1024px+
---
## Database
## 🗄️ Database Schema (SQLite)
SQLite database (`expenses.db`) created and auto-migrated on first startup.
### Tables
Auto-created on first run — 4 tables with foreign keys:
```sql
-- Users
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- OTP codes
CREATE TABLE auth_otps (
email TEXT PRIMARY KEY,
otp_code TEXT NOT NULL,
expires_at DATETIME NOT NULL
);
-- Events (expense containers)
CREATE TABLE events (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT CHECK(status IN ('open','closed')) DEFAULT 'open',
status TEXT CHECK(status IN ('open', 'closed')) DEFAULT 'open',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
-- Expenses (receipt records)
CREATE TABLE expenses (
id TEXT PRIMARY KEY,
event_id TEXT NOT NULL,
@ -248,31 +300,90 @@ CREATE TABLE expenses (
---
## PWA Features
## 🐳 Docker
- **Installable**`manifest.json` with standalone display, theme color `#10b981`
- **Offline shell** — Service Worker caches core assets (CSS, HTMX, shell) on install
- **Camera capture**`<input type="file" accept="image/*" capture="environment">` for mobile receipt snapping
```dockerfile
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache build-base
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o expenseflow .
### Service Worker
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/expenseflow .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/static ./static
COPY --from=builder /app/.env.example ./.env.example
EXPOSE 8080
CMD ["./expenseflow"]
```
- Cache name: `expenseflow-v1`
- **Install**: caches `/`, CSS, HTMX library
- **Fetch**: cache-first for static assets, network-only for API calls
```bash
docker build -t expenseflow .
docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
```
---
## Security
## 🔒 Security
| Area | Implementation |
|------|---------------|
| **Session** | In-memory store, 24h TTL, HTTP-only cookie |
| **OTP** | 5-minute expiry, 3-attempt cooldown |
| **Uploads** | Max 10 MB, JPEG/PNG only, sanitized filenames |
| **AI fallback** | Editable empty form if DeepSeek API fails |
| **Sessions** | Cryptographically random tokens (32 bytes, hex-encoded), in-memory store, 24h TTL |
| **OTP** | 6-digit codes from `crypto/rand`, 5-minute expiry, 3-fail lockout (1 minute) |
| **Cookies** | HTTP-only, SameSite=Lax, path restricted |
| **SQL Injection** | Parameterized queries on all database operations |
| **File Upload** | Magic byte validation (JPEG/PNG), 10 MB limit, sanitized filenames (UUID) |
| **Credentials** | All secrets via environment variables only — never hardcoded |
| **HTMX** | Server-rendered HTML, no client-side data exposure |
---
## License
## 🧪 Development
MIT
### Run Tests
```bash
go vet ./...
go test ./...
```
### Manual Smoke Test
```bash
# Start server
go run main.go &
# Test landing page
curl -s http://localhost:8080/ | head -5
# Test OTP request
curl -s -X POST -d "email=test@example.com" http://localhost:8080/request-otp
# Check database
sqlite3 expenses.db "SELECT * FROM auth_otps;"
```
---
## 📄 License
MIT — Free to use, modify, and distribute.
---
## 🙌 Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Commit changes (`git commit -am 'feat: add my feature'`)
4. Push (`git push origin feature/my-feature`)
5. Open a Pull Request
---
*Built with Go, HTMX, SQLite, and ❤️*

50
build-all.sh Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
#
# build-all.sh — Cross-compile ExpenseFlow for multiple platforms
#
# Prerequisites:
# linux/amd64: gcc (native)
# linux/arm64: gcc-aarch64-linux-gnu (sudo apt install gcc-aarch64-linux-gnu)
# darwin/amd64: o64-clang (via osxcross)
# darwin/arm64: aarch64-apple-darwin-clang (via osxcross)
#
# Usage: ./build-all.sh
# Output: ./dist/expenseflow-{platform}
set -euo pipefail
OUTDIR="dist"
mkdir -p "$OUTDIR"
VERSION="${1:-v1.0.0}"
LDFLAGS="-s -w"
echo "==> Building ExpenseFlow $VERSION"
build() {
local GOOS="$1" GOARCH="$2" CC="$3" SUFFIX="$4"
local OUT="$OUTDIR/expenseflow-$SUFFIX"
echo " $SUFFIX ..."
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=1 CC="$CC" \
go build -ldflags="$LDFLAGS" -o "$OUT" .
echo " => $(ls -lh "$OUT" | awk '{print $5}')"
}
# linux/amd64 — native
build linux amd64 "gcc" "linux-amd64"
# linux/arm64 — cross
if command -v aarch64-linux-gnu-gcc &>/dev/null; then
build linux arm64 "aarch64-linux-gnu-gcc" "linux-arm64"
fi
# macOS builds — only if osxcross toolchain is available
if command -v o64-clang &>/dev/null; then
build darwin amd64 "o64-clang" "darwin-amd64"
fi
if command -v aarch64-apple-darwin-clang &>/dev/null; then
build darwin arm64 "aarch64-apple-darwin-clang" "darwin-arm64"
fi
echo "==> All builds complete. Output in $OUTDIR/"
ls -lh "$OUTDIR/"