From 10c22bcaeefdcf7546e623e7ff76acbba3c51d6a Mon Sep 17 00:00:00 2001 From: cclohmar Date: Fri, 29 May 2026 19:54:20 +0000 Subject: [PATCH] chore: comprehensive README with binary distribution docs + build-all.sh --- .gitignore | 4 + README.md | 463 +++++++++++++++++++++++++++++++-------------------- build-all.sh | 50 ++++++ 3 files changed, 341 insertions(+), 176 deletions(-) create mode 100755 build-all.sh diff --git a/.gitignore b/.gitignore index bdedeea..ee22012 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,9 @@ storage/* .DS_Store Thumbs.db +# Build output +dist/ +expenseflow-* + # Go vendor/ diff --git a/README.md b/README.md index c71d063..775a4ca 100644 --- a/README.md +++ b/README.md @@ -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** — `` 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 ❤️* diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 0000000..baeefcb --- /dev/null +++ b/build-all.sh @@ -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/"