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:
parent
6de9c27f9c
commit
39fa7f6a12
2 changed files with 357 additions and 359 deletions
449
README.md
449
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
|
## 🚀 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 ❤️*
|
|
||||||
|
|
|
||||||
267
install.sh
Normal file → Executable file
267
install.sh
Normal file → Executable 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,11 +260,14 @@ echo " ╔═══════════════════════
|
||||||
echo " ║ Installation Complete! ║"
|
echo " ║ Installation Complete! ║"
|
||||||
echo " ╚═══════════════════════════════════════╝"
|
echo " ╚═══════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Install dir: $INSTALL_DIR"
|
echo " Install: $INSTALL_DIR"
|
||||||
echo " Config: $INSTALL_DIR/.env"
|
echo " Binary: $INSTALL_DIR/receiptnext"
|
||||||
echo " Binary: $BIN_PATH"
|
echo " Config: $INSTALL_DIR/.env"
|
||||||
echo " Service: systemctl status $SERVICE_NAME"
|
echo " Templates: $INSTALL_DIR/templates/"
|
||||||
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
echo " Static: $INSTALL_DIR/static/"
|
||||||
|
echo " Storage: $INSTALL_DIR/storage/"
|
||||||
|
echo " Service: systemctl status $SERVICE_NAME"
|
||||||
|
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
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)"
|
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 ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue