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

449
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 ## 🚀 One-Command Install
Pre-compiled binaries are available for download from the [Releases](https://git.lohmar.co.uk/cclohmar/ExpenseFlow/releases) page.
### Download & Run
```bash ```bash
# Linux (amd64) sudo curl -sL https://git.lohmar.co.uk/cclohmar/ReceiptNext/releases/download/v1.0.0/install.sh | bash
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. 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 ### Prerequisites
- **Go 1.22+** — [Download](https://go.dev/dl/) - **Go 1.23+** — [Download](https://go.dev/dl/)
- **GCC** (CGO is required for the SQLite driver) - No GCC, no CGO, no cross-compilers needed
```bash ### Build
# Debian/Ubuntu
sudo apt install build-essential
# macOS
xcode-select --install
# Alpine
apk add build-base
```
### Clone & Build
```bash ```bash
git clone https://git.lohmar.co.uk/cclohmar/ExpenseFlow.git git clone https://git.lohmar.co.uk/cclohmar/ReceiptNext.git
cd ExpenseFlow cd ReceiptNext
# Build for your current platform # 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 is fully static — zero runtime dependencies.
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 ## ⚙️ Configuration
Copy `.env.example` to `.env` and fill in your credentials: Configuration is via environment variables in `.env`. The install script builds this for you interactively.
```bash
cp .env.example .env
```
| Variable | Required | Default | Description | | 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 | | `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.* > **Note:** SMTP is optional without it, OTP codes are logged to the server console for testing. Reports cannot be emailed without SMTP.
### 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.
--- ---
@ -124,208 +124,119 @@ cp .env.example .env
### 1. Start the Server ### 1. Start the Server
```bash ```bash
./expenseflow # If installed via systemd:
systemctl start receiptnext
# Or run directly:
./receiptnext
``` ```
### 2. Open in Browser ### 2. Open in Browser
Navigate to [http://localhost:8080](http://localhost:8080) Navigate to `http://YOUR_SERVER:8080`
### 3. Full Acceptance Flow ### 3. Full Acceptance Flow
``` ```
1. Enter your email → click "Send Verification Code" 1. Enter your email → click "Send Verification Code"
2. Check your inbox for the 6-digit OTP code 2. Check your inbox (or server log) for the 6-digit OTP
3. Enter the OTP → click "Verify Code" 3. Enter OTP → click "Verify Code"
4. Create an event (e.g., "WebSummit 2026") 4. Click "+ New" → name your event → set claim currency → enter a conversion sample
5. Click "Add Expenses" on the event card 5. Click "Open" on the event
6. Click "Capture Receipt" → take a photo or select an image 6. Tap "📷 Camera" or "📁 Upload" to add a receipt
7. AI extracts: amount, merchant, category, date → pre-fills the form 7. AI extracts amount, merchant, category, date → form is pre-filled
8. Review and click "Save Expense" 8. Click "Save Expense"
9. Click "File Event" → enter recipient email → choose CSV or PDF 9. Click "Submit Event" → enter recipient email → choose CSV or PDF
10. Report is emailed and event status changes to "closed" 10. Report + receipt images ZIP are emailed → event auto-closes
11. Click "Reopen" to re-open a closed event 11. Click "Reopen" to add more receipts
``` ```
--- ---
## 📡 API Reference ## 📡 API Reference
### Public Endpoints (no authentication) ### Public
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `GET` | `/` | Landing page with email login form | | `GET` | `/` | Landing page |
| `POST` | `/request-otp` | Request a 6-digit OTP code (sends email) | | `POST` | `/request-otp` | Request OTP code |
| `POST` | `/verify-otp` | Verify OTP code and create session | | `POST` | `/verify-otp` | Verify OTP and create session |
| `POST` | `/logout` | Clear session and redirect to login | | `POST` | `/logout` | Clear session |
### Protected Endpoints (require session cookie) ### Protected (requires session)
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `GET` | `/dashboard` | Event dashboard | | `GET` | `/dashboard` | Event list |
| `POST` | `/events` | Create a new event | | `POST` | `/events` | Create event |
| `GET` | `/events/{id}/expenses` | View event with expense list | | `GET` | `/events/{id}/expenses` | View event + expenses |
| `PUT` | `/events/{id}/reopen` | Reopen a closed event | | `PUT` | `/events/{id}/reopen` | Reopen closed event |
| `POST` | `/expenses/upload` | Upload receipt image (multipart) | | `POST` | `/expenses/upload` | Upload receipt image/PDF |
| `POST` | `/expenses` | Save expense from form data | | `POST` | `/expenses` | Save expense |
| `POST` | `/events/{id}/file` | Generate report (CSV/PDF) and email it | | `GET` | `/expenses/{id}/edit` | Get edit form |
| `PUT` | `/expenses/{id}` | Update expense |
### Static Files | `POST` | `/events/{id}/file` | File report (CSV/PDF + images ZIP) |
| 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 |
--- ---
## 🏗️ Project Structure ## 🏗️ Project Structure
``` ```
ExpenseFlow/ /opt/rx/
├── main.go # Entry point, router, middleware, server ├── receiptnext # Compiled binary
├── go.mod / go.sum # Go module definition ├── .env # Configuration
├── .env.example # Environment variable template ├── templates/ # Go HTML templates
├── README.md # This file ├── static/ # CSS, icons, favicon, service worker
├── internal/ │ ├── css/style.css
│ ├── database/db.go # SQLite init, auto-migration, 11 query functions │ ├── favicon.svg # Rx logo
│ ├── auth/ │ ├── manifest.json
│ │ ├── otp.go # 6-digit OTP generation + 3-fail lockout │ ├── sw.js
│ │ └── session.go # In-memory session store (crypto tokens, 24h TTL) │ └── icons/
│ ├── handlers/ ├── storage/ # Uploaded receipt images (runtime)
│ │ ├── auth.go # Auth endpoints + middleware ├── install.sh # Installer script
│ │ ├── events.go # Event CRUD + dashboard ├── Makefile # Build targets
│ │ ├── expenses.go # Receipt upload, AI extraction, save └── contrib/
│ │ └── file.go # CSV/PDF generation + email filing └── receiptnext.service # Systemd service file
│ ├── 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)
``` ```
--- ---
## 🧩 Features in Detail ## 🧩 Features
### 🔐 Passwordless OTP Authentication ### 🔐 Passwordless OTP Authentication
- Email-based 6-digit code, 5-minute expiry - Email-based 6-digit code, 5-minute expiry
- Auto-creates user account on first login - 3 failed attempts → 1-minute cooldown
- 3 failed attempts trigger a 1-minute cooldown - HTTP-only session cookie, 24h TTL
- 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
### 🤖 AI Receipt Extraction ### 🤖 AI Receipt Extraction
- Upload receipt images (JPEG/PNG, max 10 MB) - **Google Gemini** (default) — cloud vision API
- DeepSeek Vision API extracts: amount, currency, merchant, category, date - **OpenAI-compatible** — works with OpenAI, Perplexity, Groq, Together AI, etc.
- Editable pre-filled form on failure or success - **Ollama** — local, offline, no API key needed (uses `qwen3.5:2b`)
- Images stored locally in `./storage/` - 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 ### 📧 Email Reporting
- Generate CSV (via `encoding/csv`) or PDF (via `gofpdf`) - CSV or PDF report
- Automatic email delivery via SMTP with file attachment - Receipt images bundled as ZIP (`expense-{event}-images.zip`)
- Event auto-closes after successful filing - Filenames: `expense-{event}-report.csv`, `expense-{event}-report.pdf`
- Multipart MIME support with proper content headers - Item numbers match between report rows and ZIP images
### 📱 Progressive Web App ### 📱 Progressive Web App
- Installable on mobile and desktop (manifest.json) - Installable on mobile home screen
- Offline shell caching (service worker) - Camera capture + gallery upload
- Camera capture for receipts (`capture="environment"`) - Dark "Terminal Mint" theme (`#0F172A` base)
- Theme color: `#10b981` (emerald green) - Rx favicon in emerald green
- Responsive design: 320px → 768px → 1024px+
--- ---
## 🗄️ Database Schema (SQLite) ## 🗄️ Database
Auto-created on first run — 4 tables with foreign keys: SQLite, auto-created on first run — 4 tables: `users`, `auth_otps`, `events`, `expenses`.
```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
```
--- ---
@ -333,57 +244,13 @@ docker run -p 8080:8080 -v $(pwd)/.env:/app/.env -v $(pwd)/storage:/app/storage
| Area | Implementation | | Area | Implementation |
|------|---------------| |------|---------------|
| **Sessions** | Cryptographically random tokens (32 bytes, hex-encoded), in-memory store, 24h TTL | | Sessions | `crypto/rand` tokens, in-memory, 24h TTL |
| **OTP** | 6-digit codes from `crypto/rand`, 5-minute expiry, 3-fail lockout (1 minute) | | OTP | `crypto/rand` codes, 5min expiry, lockout after 3 failures |
| **Cookies** | HTTP-only, SameSite=Lax, path restricted | | Cookies | HTTP-only, SameSite=Lax, path-restricted |
| **SQL Injection** | Parameterized queries on all database operations | | SQL | Parameterized queries everywhere |
| **File Upload** | Magic byte validation (JPEG/PNG), 10 MB limit, sanitized filenames (UUID) | | Uploads | Magic byte validation, max 10MB, UUID filenames |
| **Credentials** | All secrets via environment variables only — never hardcoded | | Config | `.env` file is `chmod 600` — readable only by root |
| **HTMX** | Server-rendered HTML, no client-side data exposure |
--- ---
## 🧪 Development *Built with Go, HTMX, SQLite and ❤️*
### 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 ❤️*

259
install.sh Normal file → Executable file
View file

@ -1,18 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# install.sh — ReceiptNext installer # ReceiptNext Installer
# =====================
# #
# Copies the binary and assets to /opt/rx/, prompts for configuration, # Usage:
# and sets up a systemd service. # 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 set -euo pipefail
INSTALL_DIR="/opt/rx" INSTALL_DIR="/opt/rx"
BIN_PATH="/opt/rx/receiptnext"
SERVICE_NAME="receiptnext" SERVICE_NAME="receiptnext"
REPO_URL="https://git.lohmar.co.uk/cclohmar/ReceiptNext.git"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@ -22,98 +24,218 @@ NC='\033[0m'
info() { echo -e "${GREEN}[✓]${NC} $1"; } info() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${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 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 exit 1
fi 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 " ╔═══════════════════════════════════════╗" echo " ╔═══════════════════════════════════════╗"
echo " ║ ReceiptNext Installer ║" echo " ║ ReceiptNext Installer ║"
echo " ╚═══════════════════════════════════════╝" echo " ╚═══════════════════════════════════════╝"
echo "" echo ""
# ── Detect binary ──────────────────────────────────────────────── # ── 1. Clone repo ────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" if [ -d "$INSTALL_DIR" ]; then
BINARY="" warn "$INSTALL_DIR already exists — pulling latest..."
for f in "$SCRIPT_DIR/receiptnext" "$SCRIPT_DIR/dist/receiptnext-linux-amd64" "$SCRIPT_DIR/dist/receiptnext-linux-arm64"; do cd "$INSTALL_DIR"
if [ -f "$f" ] && [ -x "$f" ]; then git pull
BINARY="$f" else
break info "Cloning repository to $INSTALL_DIR..."
fi git clone "$REPO_URL" "$INSTALL_DIR"
done cd "$INSTALL_DIR"
if [ -z "$BINARY" ]; then
echo -e "${RED}No binary found. Build it first: go build -o receiptnext .${NC}"
exit 1
fi fi
BINARY_NAME=$(basename "$BINARY") # ── 2. Build binary ──────────────────────────────────────────────
info "Using binary: $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 ───────────────────────────────────── # ── 3. Create runtime directories ────────────────────────────────
mkdir -p "$INSTALL_DIR"/{templates,static/css,static/icons,storage} mkdir -p storage
info "Created $INSTALL_DIR" chmod 755 templates static storage
# ── Copy binary and assets ─────────────────────────────────────── # ── 4. AI Provider ───────────────────────────────────────────────
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 ─────────────────────────────────────
echo "" echo ""
echo " ── Configuration ──" echo " ── AI Provider ──"
echo " (Press Enter to skip any field)" 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 "" 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 case "$AI_CHOICE" in
SMTP_HOST="${SMTP_HOST_IN:-$SMTP_HOST}" 2)
read -p " SMTP port [${SMTP_PORT:-587}]: " SMTP_PORT_IN AI_PROVIDER="openai"
SMTP_PORT="${SMTP_PORT_IN:-${SMTP_PORT:-587}}" ask " OpenAI API key: "
read -p " SMTP user [$SMTP_USER]: " SMTP_USER_IN read -r OPENAI_API_KEY
SMTP_USER="${SMTP_USER_IN:-$SMTP_USER}" ask " Model [gpt-4o-mini]: "
read -p " SMTP password [$SMTP_PASS]: " SMTP_PASS_IN read -r AI_MODEL
SMTP_PASS="${SMTP_PASS_IN:-$SMTP_PASS}" AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
read -p " Gemini API key [$GEMINI_API_KEY]: " GEMINI_IN ask " Base URL [https://api.openai.com/v1]: "
GEMINI_API_KEY="${GEMINI_IN:-$GEMINI_API_KEY}" read -r AI_BASE_URL
read -p " Public server URL [${BASE_URL:-http://localhost:8080}]: " BASE_URL_IN AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
BASE_URL="${BASE_URL_IN:-${BASE_URL:-http://localhost:8080}}" ;;
read -p " Port for web server [${PORT:-8080}]: " PORT_IN 3)
PORT="${PORT_IN:-${PORT:-8080}}" 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 cat > "$INSTALL_DIR/.env" << ENVEOF
# ReceiptNext Configuration # 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_HOST=${SMTP_HOST}
SMTP_PORT=${SMTP_PORT} SMTP_PORT=${SMTP_PORT}
SMTP_USER=${SMTP_USER} SMTP_USER=${SMTP_USER}
SMTP_PASS=${SMTP_PASS} SMTP_PASS=${SMTP_PASS}
GEMINI_API_KEY=${GEMINI_API_KEY}
BASE_URL=${BASE_URL}
PORT=${PORT}
ENVEOF ENVEOF
fi
chmod 600 "$INSTALL_DIR/.env" 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 cat > "/etc/systemd/system/${SERVICE_NAME}.service" << SERVEOF
[Unit] [Unit]
Description=ReceiptNext — AI-Powered Expense Tracker Description=ReceiptNext — AI-Powered Expense Tracker
Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext Documentation=https://git.lohmar.co.uk/cclohmar/ReceiptNext
After=network.target After=network.target ollama.service
Wants=ollama.service
[Service] [Service]
Type=simple Type=simple
User=root User=root
WorkingDirectory=${INSTALL_DIR} WorkingDirectory=${INSTALL_DIR}
ExecStart=${BIN_PATH} ExecStart=${INSTALL_DIR}/receiptnext
Restart=always Restart=always
RestartSec=5 RestartSec=5
EnvironmentFile=${INSTALL_DIR}/.env EnvironmentFile=${INSTALL_DIR}/.env
@ -126,10 +248,11 @@ SERVEOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable "${SERVICE_NAME}" systemctl enable "${SERVICE_NAME}"
info "Systemd service created: ${SERVICE_NAME}" info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
# ── Start service ──────────────────────────────────────────────── # ── 9. Start ─────────────────────────────────────────────────────
systemctl start "${SERVICE_NAME}" 2>/dev/null || true info "Starting ReceiptNext..."
systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
sleep 2 sleep 2
echo "" echo ""
@ -137,9 +260,12 @@ echo " ╔═══════════════════════
echo " ║ Installation Complete! ║" echo " ║ Installation Complete! ║"
echo " ╚═══════════════════════════════════════╝" echo " ╚═══════════════════════════════════════╝"
echo "" echo ""
echo " Install dir: $INSTALL_DIR" echo " Install: $INSTALL_DIR"
echo " Binary: $INSTALL_DIR/receiptnext"
echo " Config: $INSTALL_DIR/.env" 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 " Service: systemctl status $SERVICE_NAME"
echo " Logs: journalctl -u $SERVICE_NAME -f" echo " Logs: journalctl -u $SERVICE_NAME -f"
echo "" echo ""
@ -149,5 +275,10 @@ if systemctl is-active --quiet "${SERVICE_NAME}"; then
echo " Open http://localhost:${PORT} (or your server IP)" echo " Open http://localhost:${PORT} (or your server IP)"
else else
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}" 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 fi
echo ""
echo " ── Update ──"
echo " To update later: sudo ./install.sh -update"
echo ""