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 .DS_Store
Thumbs.db Thumbs.db
# Build output
dist/
expenseflow-*
# Go # Go
vendor/ 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 Pre-compiled binaries are available for download from the [Releases](https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases) page.
- **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 ### Download & Run
- **Event Filing** — Generate CSV or PDF report → email as attachment → auto-close event
- **PWA** — Installable on mobile homescreen, offline shell, camera capture for receipts ```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 ## 🔧 Building from Source
| 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
### Prerequisites ### Prerequisites
- Go 1.21+ - **Go 1.22+** — [Download](https://go.dev/dl/)
- GCC (required for CGO/SQLite via `mattn/go-sqlite3`) - **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 ```bash
# Clone the repository git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
git clone https://github.com/your-org/expenseflow.git cd ExpenseFlow
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 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 | ```bash
|----------|-------------|---------| ./expenseflow
| `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` |
--- ### 2. Open in Browser
## Project Structure Navigate to [http://localhost:8080](http://localhost:8080)
### 3. Full Acceptance Flow
``` ```
ExpenseFlow/ 1. Enter your email → click "Send Verification Code"
├── main.go # Entry point, router, static file server 2. Check your inbox for the 6-digit OTP code
├── go.mod # Module dependencies 3. Enter the OTP → click "Verify Code"
├── .env # Credentials (not committed) 4. Create an event (e.g., "WebSummit 2026")
├── .env.example # Env template 5. Click "Add Expenses" on the event card
├── internal/ 6. Click "Capture Receipt" → take a photo or select an image
│ ├── ai/ 7. AI extracts: amount, merchant, category, date → pre-fills the form
│ │ └── deepseek.go # DeepSeek Vision API client 8. Review and click "Save Expense"
│ ├── auth/ 9. Click "File Event" → enter recipient email → choose CSV or PDF
│ │ ├── otp.go # OTP generation & validation 10. Report is emailed and event status changes to "closed"
│ │ └── session.go # Session management (in-memory store) 11. Click "Reopen" to re-open a closed event
│ ├── 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)
``` ```
--- ---
## API Endpoints ## 📡 API Reference
### Public Endpoints (no authentication)
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `GET` | `/` | Landing page — email input for OTP login | | `GET` | `/` | Landing page with email login form |
| `POST` | `/request-otp` | Request a 6-digit OTP code (sent via email) | | `POST` | `/request-otp` | Request a 6-digit OTP code (sends email) |
| `POST` | `/verify-otp` | Verify OTP and create session cookie | | `POST` | `/verify-otp` | Verify OTP code and create session |
| `GET` | `/dashboard` | User dashboard — list of events | | `POST` | `/logout` | Clear session and redirect to login |
| `POST` | `/events` | Create a new expense event |
| `GET` | `/events/{id}/expenses` | View expenses for an event | ### Protected Endpoints (require session cookie)
| `POST` | `/expenses/upload` | Upload receipt image → AI extraction |
| `POST` | `/expenses` | Save an expense record | | Method | Path | Description |
| `POST` | `/events/{id}/file` | File report (CSV/PDF) and close event | |--------|------|-------------|
| `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 | | `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
``` ```
ExpenseFlow/
### 2. Event Dashboard ├── main.go # Entry point, router, middleware, server
├── go.mod / go.sum # Go module definition
- **Open events** — click to view expenses and capture receipts ├── .env.example # Environment variable template
- **Closed events** — show a **Reopen** button (`PUT /events/{id}/reopen`) ├── README.md # This file
- Each card shows: event name, status badge, created date ├── internal/
│ ├── database/db.go # SQLite init, auto-migration, 11 query functions
### 3. AI Receipt Capture │ ├── auth/
│ │ ├── otp.go # 6-digit OTP generation + 3-fail lockout
```bash │ │ └── session.go # In-memory session store (crypto tokens, 24h TTL)
# Upload a receipt image │ ├── handlers/
curl -X POST http://localhost:8080/expenses/upload \ │ │ ├── auth.go # Auth endpoints + middleware
-H "Cookie: session_token=..." \ │ │ ├── events.go # Event CRUD + dashboard
-F "image=@receipt.jpg" │ │ ├── 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 ### 📋 Event-Based Expense Tracking
2. **Enter email** — receive 6-digit OTP in inbox - Group expenses into events (trips, conferences, months)
3. **Enter OTP** — redirected to dashboard (empty) - Open/closed lifecycle with reopen support
4. **Create event** — name it "Test Event" - Dashboard with event cards showing name, status, and creation date
5. **Click event** — see expense list (empty) + **Capture Receipt** button
6. **Upload receipt** — snap/take a photo → AI extracts amount, merchant, category, date ### 🤖 AI Receipt Extraction
7. **Review & save** — pre-filled form, click Save → expense appears in list - Upload receipt images (JPEG/PNG, max 10 MB)
8. **File event** — click **File Event**, enter email, pick PDF → report sent, event closes - DeepSeek Vision API extracts: amount, currency, merchant, category, date
9. **Verify** — event shows as **closed** on dashboard - Editable pre-filled form on failure or success
10. **Reopen** — click **Reopen** → badge changes back to **open** - 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. Auto-created on first run — 4 tables with foreign keys:
### Tables
```sql ```sql
-- Users
CREATE TABLE users ( CREATE TABLE users (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
-- OTP codes
CREATE TABLE auth_otps ( CREATE TABLE auth_otps (
email TEXT PRIMARY KEY, email TEXT PRIMARY KEY,
otp_code TEXT NOT NULL, otp_code TEXT NOT NULL,
expires_at DATETIME NOT NULL expires_at DATETIME NOT NULL
); );
-- Events (expense containers)
CREATE TABLE events ( CREATE TABLE events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
name 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, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id) FOREIGN KEY(user_id) REFERENCES users(id)
); );
-- Expenses (receipt records)
CREATE TABLE expenses ( CREATE TABLE expenses (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
@ -248,31 +300,90 @@ CREATE TABLE expenses (
--- ---
## PWA Features ## 🐳 Docker
- **Installable**`manifest.json` with standalone display, theme color `#10b981` ```dockerfile
- **Offline shell** — Service Worker caches core assets (CSS, HTMX, shell) on install FROM golang:1.22-alpine AS builder
- **Camera capture**`<input type="file" accept="image/*" capture="environment">` for mobile receipt snapping 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` ```bash
- **Install**: caches `/`, CSS, HTMX library docker build -t expenseflow .
- **Fetch**: cache-first for static assets, network-only for API calls docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
```
--- ---
## Security ## 🔒 Security
| Area | Implementation | | Area | Implementation |
|------|---------------| |------|---------------|
| **Session** | In-memory store, 24h TTL, HTTP-only cookie | | **Sessions** | Cryptographically random tokens (32 bytes, hex-encoded), in-memory store, 24h TTL |
| **OTP** | 5-minute expiry, 3-attempt cooldown | | **OTP** | 6-digit codes from `crypto/rand`, 5-minute expiry, 3-fail lockout (1 minute) |
| **Uploads** | Max 10 MB, JPEG/PNG only, sanitized filenames | | **Cookies** | HTTP-only, SameSite=Lax, path restricted |
| **AI fallback** | Editable empty form if DeepSeek API fails | | **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/"