chore: rewrite install.sh with AI provider choice + -update flag + pure Go

- install.sh now:
  • Clones repo to /opt/rx
  • Builds pure-Go binary (no CGO)
  • Prompts AI choice: Gemini / OpenAI / Ollama
  • If Ollama: auto-installs Ollama + pulls qwen3.5:2b
  • Creates .env interactively
  • Creates systemd service
  • Supports -update flag (pull, rebuild, restart)
- README.md fully rewritten for new install flow
- All binaries removed from git tracking (dist/ in .gitignore)
This commit is contained in:
Claus Lohmar 2026-05-30 15:03:39 +00:00
parent 6de9c27f9c
commit 39fa7f6a12
2 changed files with 357 additions and 359 deletions

451
README.md
View file

@ -1,42 +1,71 @@
# ExpenseFlow — AI-Powered Expense Tracker
# ReceiptNext — AI-Powered Expense Tracker
> 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).
> A production-ready, mobile-first Progressive Web App (PWA) for expense management with passwordless OTP login, AI receipt extraction (Gemini / OpenAI / Ollama), and CSV/PDF email reporting with receipt images.
**Tech Stack:** Go 1.22+ · HTMX · SQLite · DeepSeek Vision API · PWA
**Tech Stack:** Go 1.23+ · HTMX · SQLite (pure Go) · Google Gemini / OpenAI / Ollama · PWA
---
## 📦 Quick Start — Binary Distribution
Pre-compiled binaries are available for download from the [Releases](https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases) page.
### Download & Run
## 🚀 One-Command Install
```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
sudo curl -sL https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/install.sh | bash
```
The server starts on `http://localhost:8080` by default. Set `PORT=3000` to change the port.
Or from a local clone:
> **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.
```bash
sudo ./install.sh
```
The installer will:
1. Clone the repo to `/opt/rx/`
2. Build the binary (pure Go, no CGO, no dependencies)
3. Ask which AI provider to use:
```
1) Google Gemini (cloud API, needs API key)
2) OpenAI / Compatible (OpenAI, Perplexity, Groq, etc.)
3) Ollama (local — installs Ollama + qwen3.5:2b automatically)
```
4. Prompt for SMTP settings (for OTP emails and report delivery)
5. Create `/opt/rx/.env` with all configuration
6. Set up a systemd service that auto-starts on boot
7. Start ReceiptNext
**Result:** A fully configured, always-running expense tracker at `http://YOUR_SERVER:8080`.
---
## 🔄 Updating
```bash
sudo /opt/rx/install.sh -update
```
This pulls the latest code, rebuilds the binary, and restarts the service.
---
## 📦 Binary Download (manual)
Pre-compiled static binaries are on the [Releases page](https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases):
```bash
# Linux amd64
curl -L -o receiptnext https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/receiptnext-linux-amd64
chmod +x receiptnext
./receiptnext
# Linux arm64 (Raspberry Pi, etc.)
curl -L -o receiptnext https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/receiptnext-linux-arm64
chmod +x receiptnext
./receiptnext
```
No dependencies, no CGO, no libc. Drop it on any Linux box and run.
---
@ -44,78 +73,49 @@ The server starts on `http://localhost:8080` by default. Set `PORT=3000` to chan
### Prerequisites
- **Go 1.22+** — [Download](https://go.dev/dl/)
- **GCC** (CGO is required for the SQLite driver)
- **Go 1.23+** — [Download](https://go.dev/dl/)
- No GCC, no CGO, no cross-compilers needed
### Build
```bash
# Debian/Ubuntu
sudo apt install build-essential
# macOS
xcode-select --install
# Alpine
apk add build-base
```
### Clone & Build
```bash
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git
cd ExpenseFlow
git clone https://git.lohmar.co.uk/cclohmar/ReceiptNext.git
cd ReceiptNext
# Build for your current platform
go build -o expenseflow .
CGO_ENABLED=0 go build -o receiptnext .
# The binary is now ready: ./expenseflow
# Cross-compile for any platform (no extra tools needed!)
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o receiptnext-linux-arm64 .
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o receiptnext-darwin-amd64 .
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o receiptnext-darwin-arm64 .
```
### 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`.
The binary is fully static — zero runtime dependencies.
---
## ⚙️ Configuration
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cp .env.example .env
```
Configuration is via environment variables in `.env`. The install script builds this for you interactively.
| 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 |
| `BASE_URL` | No | `http://localhost:8080` | Public URL for email links |
| **AI Provider** | | | |
| `AI_PROVIDER` | No | `gemini` | `gemini`, `openai`, or `ollama` |
| `GEMINI_API_KEY` | For Gemini | — | Google Gemini API key |
| `OPENAI_API_KEY` | For OpenAI | — | OpenAI-compatible API key |
| `AI_MODEL` | For OpenAI/Ollama | `gpt-4o-mini` / `qwen3.5:2b` | Model name |
| `AI_BASE_URL` | For OpenAI/Ollama | `https://api.openai.com/v1` | API endpoint |
| **SMTP** | | | |
| `SMTP_HOST` | See note | — | SMTP server hostname |
| `SMTP_PORT` | See note | `587` | SMTP server port |
| `SMTP_USER` | See note | — | SMTP username |
| `SMTP_PASS` | See note | — | SMTP password |
*\* 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.
> **Note:** SMTP is optional without it, OTP codes are logged to the server console for testing. Reports cannot be emailed without SMTP.
---
@ -124,208 +124,119 @@ cp .env.example .env
### 1. Start the Server
```bash
./expenseflow
# If installed via systemd:
systemctl start receiptnext
# Or run directly:
./receiptnext
```
### 2. Open in Browser
Navigate to [http://localhost:8080](http://localhost:8080)
Navigate to `http://YOUR_SERVER:8080`
### 3. Full Acceptance Flow
```
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
2. Check your inbox (or server log) for the 6-digit OTP
3. Enter OTP → click "Verify Code"
4. Click "+ New" → name your event → set claim currency → enter a conversion sample
5. Click "Open" on the event
6. Tap "📷 Camera" or "📁 Upload" to add a receipt
7. AI extracts amount, merchant, category, date → form is pre-filled
8. Click "Save Expense"
9. Click "Submit Event" → enter recipient email → choose CSV or PDF
10. Report + receipt images ZIP are emailed → event auto-closes
11. Click "Reopen" to add more receipts
```
---
## 📡 API Reference
### Public Endpoints (no authentication)
### Public
| Method | Path | Description |
|--------|------|-------------|
| `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 |
| `GET` | `/` | Landing page |
| `POST` | `/request-otp` | Request OTP code |
| `POST` | `/verify-otp` | Verify OTP and create session |
| `POST` | `/logout` | Clear session |
### Protected Endpoints (require session cookie)
### Protected (requires session)
| 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 |
| `GET` | `/dashboard` | Event list |
| `POST` | `/events` | Create event |
| `GET` | `/events/{id}/expenses` | View event + expenses |
| `PUT` | `/events/{id}/reopen` | Reopen closed event |
| `POST` | `/expenses/upload` | Upload receipt image/PDF |
| `POST` | `/expenses` | Save expense |
| `GET` | `/expenses/{id}/edit` | Get edit form |
| `PUT` | `/expenses/{id}` | Update expense |
| `POST` | `/events/{id}/file` | File report (CSV/PDF + images ZIP) |
---
## 🏗️ Project Structure
```
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)
/opt/rx/
├── receiptnext # Compiled binary
├── .env # Configuration
├── templates/ # Go HTML templates
├── static/ # CSS, icons, favicon, service worker
│ ├── css/style.css
│ ├── favicon.svg # Rx logo
│ ├── manifest.json
│ ├── sw.js
│ └── icons/
├── storage/ # Uploaded receipt images (runtime)
├── install.sh # Installer script
├── Makefile # Build targets
└── contrib/
└── receiptnext.service # Systemd service file
```
---
## 🧩 Features in Detail
## 🧩 Features
### 🔐 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
### 📋 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
- 3 failed attempts → 1-minute cooldown
- HTTP-only session cookie, 24h TTL
### 🤖 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/`
- **Google Gemini** (default) — cloud vision API
- **OpenAI-compatible** — works with OpenAI, Perplexity, Groq, Together AI, etc.
- **Ollama** — local, offline, no API key needed (uses `qwen3.5:2b`)
- Supports: JPEG, PNG, WebP, HEIC, PDF (email receipts from Uber, etc.)
### 💱 Currency Conversion
- Sample-based: enter a real receipt amount and what you were charged
- System computes the rate automatically
- Each expense stores original + converted amount
### 📧 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
- CSV or PDF report
- Receipt images bundled as ZIP (`expense-{event}-images.zip`)
- Filenames: `expense-{event}-report.csv`, `expense-{event}-report.pdf`
- Item numbers match between report rows and ZIP images
### 📱 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+
- Installable on mobile home screen
- Camera capture + gallery upload
- Dark "Terminal Mint" theme (`#0F172A` base)
- Rx favicon in emerald green
---
## 🗄️ Database Schema (SQLite)
## 🗄️ Database
Auto-created on first run — 4 tables with foreign keys:
```sql
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE auth_otps (
email TEXT PRIMARY KEY,
otp_code TEXT NOT NULL,
expires_at DATETIME NOT NULL
);
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',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE TABLE expenses (
id TEXT PRIMARY KEY,
event_id TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT NOT NULL,
merchant TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT,
date TEXT NOT NULL,
image_path TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE
);
```
---
## 🐳 Docker
```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 .
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"]
```
```bash
docker build -t expenseflow .
docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage expenseflow
```
SQLite, auto-created on first run — 4 tables: `users`, `auth_otps`, `events`, `expenses`.
---
@ -333,57 +244,13 @@ docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage
| Area | Implementation |
|------|---------------|
| **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 |
| Sessions | `crypto/rand` tokens, in-memory, 24h TTL |
| OTP | `crypto/rand` codes, 5min expiry, lockout after 3 failures |
| Cookies | HTTP-only, SameSite=Lax, path-restricted |
| SQL | Parameterized queries everywhere |
| Uploads | Magic byte validation, max 10MB, UUID filenames |
| Config | `.env` file is `chmod 600` — readable only by root |
---
## 🧪 Development
### 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 ❤️*
*Built with Go, HTMX, SQLite and ❤️*

259
install.sh Normal file → Executable file
View file

@ -1,18 +1,20 @@
#!/usr/bin/env bash
#
# install.sh — ReceiptNext installer
# ReceiptNext Installer
# =====================
#
# Copies the binary and assets to /opt/rx/, prompts for configuration,
# and sets up a systemd service.
# Usage:
# sudo ./install.sh — Full install (clone, build, configure, service)
# sudo ./install.sh -update — Pull latest, rebuild, restart
#
# Usage: sudo ./install.sh
# Installs to /opt/rx/ and sets up a systemd service.
#
set -euo pipefail
INSTALL_DIR="/opt/rx"
BIN_PATH="/opt/rx/receiptnext"
SERVICE_NAME="receiptnext"
REPO_URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext.git"
RED='\033[0;31m'
GREEN='\033[0;32m'
@ -22,98 +24,218 @@ NC='\033[0m'
info() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
prompt() { echo -en "${CYAN}${NC} $1"; }
err() { echo -e "${RED}[✗]${NC} $1"; }
ask() { echo -en "${CYAN}${NC} $1"; }
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root: sudo ./install.sh${NC}"
err "Please run as root: sudo ./install.sh"
exit 1
fi
# ──────────────────────────────────────────────────────────────────
# UPDATE MODE
# ──────────────────────────────────────────────────────────────────
if [ "${1:-}" = "-update" ]; then
echo ""
echo " ╔═══════════════════════════════════════╗"
echo " ║ ReceiptNext — Update Mode ║"
echo " ╚═══════════════════════════════════════╝"
echo ""
if [ ! -d "$INSTALL_DIR" ]; then
err "$INSTALL_DIR does not exist. Run install.sh without -update first."
exit 1
fi
cd "$INSTALL_DIR"
if [ -d .git ]; then
info "Pulling latest code..."
git pull
else
warn "Not a git repository — re-cloning..."
cd /tmp
rm -rf receiptnext-update
git clone "$REPO_URL" receiptnext-update
rsync -a --delete receiptnext-update/ "$INSTALL_DIR/"
rm -rf receiptnext-update
cd "$INSTALL_DIR"
fi
info "Rebuilding binary..."
CGO_ENABLED=0 go build -ldflags="-s -w" -o receiptnext .
info "Restarting service..."
systemctl restart "$SERVICE_NAME"
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
info "Update complete — ReceiptNext is running"
else
warn "Service did not start. Check: systemctl status $SERVICE_NAME"
fi
exit 0
fi
# ──────────────────────────────────────────────────────────────────
# FRESH INSTALL
# ──────────────────────────────────────────────────────────────────
echo ""
echo " ╔═══════════════════════════════════════╗"
echo " ║ ReceiptNext Installer ║"
echo " ╚═══════════════════════════════════════╝"
echo ""
# ── Detect binary ────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BINARY=""
for f in "$SCRIPT_DIR/receiptnext" "$SCRIPT_DIR/dist/receiptnext-linux-amd64" "$SCRIPT_DIR/dist/receiptnext-linux-arm64"; do
if [ -f "$f" ] && [ -x "$f" ]; then
BINARY="$f"
break
fi
done
if [ -z "$BINARY" ]; then
echo -e "${RED}No binary found. Build it first: go build -o receiptnext .${NC}"
exit 1
# ── 1. Clone repo ────────────────────────────────────────────────
if [ -d "$INSTALL_DIR" ]; then
warn "$INSTALL_DIR already exists — pulling latest..."
cd "$INSTALL_DIR"
git pull
else
info "Cloning repository to $INSTALL_DIR..."
git clone "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR"
fi
BINARY_NAME=$(basename "$BINARY")
info "Using binary: $BINARY"
# ── 2. Build binary ──────────────────────────────────────────────
info "Building binary (pure Go, no CGO)..."
CGO_ENABLED=0 go build -ldflags="-s -w" -o receiptnext .
info "Binary built: $INSTALL_DIR/receiptnext"
# ── Create install directory ─────────────────────────────────────
mkdir -p "$INSTALL_DIR"/{templates,static/css,static/icons,storage}
info "Created $INSTALL_DIR"
# ── 3. Create runtime directories ────────────────────────────────
mkdir -p storage
chmod 755 templates static storage
# ── Copy binary and assets ───────────────────────────────────────
cp "$BINARY" "$BIN_PATH"
chmod +x "$BIN_PATH"
cp -r "$SCRIPT_DIR/templates/." "$INSTALL_DIR/templates/"
cp -r "$SCRIPT_DIR/static/." "$INSTALL_DIR/static/"
info "Copied binary + assets to $INSTALL_DIR"
# ── Prompt for configuration ─────────────────────────────────────
# ── 4. AI Provider ───────────────────────────────────────────────
echo ""
echo " ── Configuration ──"
echo " (Press Enter to skip any field)"
echo " ── AI Provider ──"
echo " Which AI should process receipt images?"
echo " 1) Google Gemini (cloud API, needs API key)"
echo " 2) OpenAI / Compatible (OpenAI, Perplexity, Groq, etc.)"
echo " 3) Ollama (local, installs automatically)"
echo ""
ask " Choose [1-3] (default: 1): "
read -r AI_CHOICE
AI_CHOICE="${AI_CHOICE:-1}"
read -p " SMTP host [$SMTP_HOST]: " SMTP_HOST_IN
SMTP_HOST="${SMTP_HOST_IN:-$SMTP_HOST}"
read -p " SMTP port [${SMTP_PORT:-587}]: " SMTP_PORT_IN
SMTP_PORT="${SMTP_PORT_IN:-${SMTP_PORT:-587}}"
read -p " SMTP user [$SMTP_USER]: " SMTP_USER_IN
SMTP_USER="${SMTP_USER_IN:-$SMTP_USER}"
read -p " SMTP password [$SMTP_PASS]: " SMTP_PASS_IN
SMTP_PASS="${SMTP_PASS_IN:-$SMTP_PASS}"
read -p " Gemini API key [$GEMINI_API_KEY]: " GEMINI_IN
GEMINI_API_KEY="${GEMINI_IN:-$GEMINI_API_KEY}"
read -p " Public server URL [${BASE_URL:-http://localhost:8080}]: " BASE_URL_IN
BASE_URL="${BASE_URL_IN:-${BASE_URL:-http://localhost:8080}}"
read -p " Port for web server [${PORT:-8080}]: " PORT_IN
PORT="${PORT_IN:-${PORT:-8080}}"
case "$AI_CHOICE" in
2)
AI_PROVIDER="openai"
ask " OpenAI API key: "
read -r OPENAI_API_KEY
ask " Model [gpt-4o-mini]: "
read -r AI_MODEL
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
ask " Base URL [https://api.openai.com/v1]: "
read -r AI_BASE_URL
AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
;;
3)
AI_PROVIDER="ollama"
AI_MODEL="qwen3.5:2b"
info "Installing Ollama..."
if ! command -v ollama &>/dev/null; then
curl -fsSL https://ollama.com/install.sh | sh 2>&1 | tail -3
else
info "Ollama already installed"
fi
info "Pulling model $AI_MODEL (this may take a while)..."
ollama pull "$AI_MODEL" 2>&1 | tail -3
AI_BASE_URL="http://localhost:11434"
;;
*)
AI_PROVIDER="gemini"
ask " Gemini API key: "
read -r GEMINI_API_KEY
;;
esac
# ── Write .env ───────────────────────────────────────────────────
# ── 5. SMTP Configuration ────────────────────────────────────────
echo ""
echo " ── Email (SMTP) ──"
echo " Required for sending OTP codes and expense reports."
echo " Leave blank to skip (OTP codes will be logged to console)."
echo ""
ask " SMTP host: "
read -r SMTP_HOST
if [ -n "$SMTP_HOST" ]; then
ask " SMTP port [587]: "
read -r SMTP_PORT
SMTP_PORT="${SMTP_PORT:-587}"
ask " SMTP user: "
read -r SMTP_USER
ask " SMTP password: "
read -r SMTP_PASS
fi
# ── 6. General settings ──────────────────────────────────────────
echo ""
echo " ── General ──"
ask " Public URL [http://localhost:8080]: "
read -r BASE_URL
BASE_URL="${BASE_URL:-http://localhost:8080}"
ask " Web server port [8080]: "
read -r PORT
PORT="${PORT:-8080}"
# ── 7. Write .env ────────────────────────────────────────────────
info "Creating .env..."
cat > "$INSTALL_DIR/.env" << ENVEOF
# ReceiptNext Configuration
# Created by install.sh on $(date)
# Generated by install.sh on $(date)
PORT=${PORT}
BASE_URL=${BASE_URL}
AI_PROVIDER=${AI_PROVIDER}
ENVEOF
case "$AI_PROVIDER" in
openai)
cat >> "$INSTALL_DIR/.env" << ENVEOF
OPENAI_API_KEY=${OPENAI_API_KEY}
AI_MODEL=${AI_MODEL}
AI_BASE_URL=${AI_BASE_URL}
ENVEOF
;;
ollama)
cat >> "$INSTALL_DIR/.env" << ENVEOF
AI_MODEL=${AI_MODEL}
AI_BASE_URL=${AI_BASE_URL}
ENVEOF
;;
gemini)
cat >> "$INSTALL_DIR/.env" << ENVEOF
GEMINI_API_KEY=${GEMINI_API_KEY}
ENVEOF
;;
esac
if [ -n "$SMTP_HOST" ]; then
cat >> "$INSTALL_DIR/.env" << ENVEOF
SMTP_HOST=${SMTP_HOST}
SMTP_PORT=${SMTP_PORT}
SMTP_USER=${SMTP_USER}
SMTP_PASS=${SMTP_PASS}
GEMINI_API_KEY=${GEMINI_API_KEY}
BASE_URL=${BASE_URL}
PORT=${PORT}
ENVEOF
fi
chmod 600 "$INSTALL_DIR/.env"
info "Created $INSTALL_DIR/.env"
info "Configuration saved to $INSTALL_DIR/.env"
# ── Create systemd service ───────────────────────────────────────
# ── 8. Systemd service ───────────────────────────────────────────
info "Creating systemd service..."
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << SERVEOF
[Unit]
Description=ReceiptNext — AI-Powered Expense Tracker
Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext
After=network.target
After=network.target ollama.service
Wants=ollama.service
[Service]
Type=simple
User=root
WorkingDirectory=${INSTALL_DIR}
ExecStart=${BIN_PATH}
ExecStart=${INSTALL_DIR}/receiptnext
Restart=always
RestartSec=5
EnvironmentFile=${INSTALL_DIR}/.env
@ -126,10 +248,11 @@ SERVEOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
info "Systemd service created: ${SERVICE_NAME}"
info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
# ── Start service ────────────────────────────────────────────────
systemctl start "${SERVICE_NAME}" 2>/dev/null || true
# ── 9. Start ─────────────────────────────────────────────────────
info "Starting ReceiptNext..."
systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
sleep 2
echo ""
@ -137,9 +260,12 @@ echo " ╔═══════════════════════
echo " ║ Installation Complete! ║"
echo " ╚═══════════════════════════════════════╝"
echo ""
echo " Install dir: $INSTALL_DIR"
echo " Install: $INSTALL_DIR"
echo " Binary: $INSTALL_DIR/receiptnext"
echo " Config: $INSTALL_DIR/.env"
echo " Binary: $BIN_PATH"
echo " Templates: $INSTALL_DIR/templates/"
echo " Static: $INSTALL_DIR/static/"
echo " Storage: $INSTALL_DIR/storage/"
echo " Service: systemctl status $SERVICE_NAME"
echo " Logs: journalctl -u $SERVICE_NAME -f"
echo ""
@ -149,5 +275,10 @@ if systemctl is-active --quiet "${SERVICE_NAME}"; then
echo " Open http://localhost:${PORT} (or your server IP)"
else
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}"
echo " Logs: journalctl -u ${SERVICE_NAME} -n 50 --no-pager"
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager
fi
echo ""
echo " ── Update ──"
echo " To update later: sudo ./install.sh -update"
echo ""