From 39fa7f6a12bcc27a4197dc8d7bed5c1a9e7c7771 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Sat, 30 May 2026 15:03:39 +0000 Subject: [PATCH] chore: rewrite install.sh with AI provider choice + -update flag + pure Go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- README.md | 449 +++++++++++++++++++---------------------------------- install.sh | 267 +++++++++++++++++++++++-------- 2 files changed, 357 insertions(+), 359 deletions(-) mode change 100644 => 100755 install.sh diff --git a/README.md b/README.md index 775a4ca..2daf545 100644 --- a/README.md +++ b/README.md @@ -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 - ```bash - # Debian/Ubuntu - sudo apt install build-essential - - # macOS - xcode-select --install - - # Alpine - apk add build-base - ``` - -### Clone & Build +### 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 ❤️* diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index f5e5a13..1838e09 --- a/install.sh +++ b/install.sh @@ -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 " ║ 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,11 +260,14 @@ echo " ╔═══════════════════════ echo " ║ Installation Complete! ║" echo " ╚═══════════════════════════════════════╝" echo "" -echo " Install dir: $INSTALL_DIR" -echo " Config: $INSTALL_DIR/.env" -echo " Binary: $BIN_PATH" -echo " Service: systemctl status $SERVICE_NAME" -echo " Logs: journalctl -u $SERVICE_NAME -f" +echo " Install: $INSTALL_DIR" +echo " Binary: $INSTALL_DIR/receiptnext" +echo " Config: $INSTALL_DIR/.env" +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 "" if systemctl is-active --quiet "${SERVICE_NAME}"; then @@ -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 ""