diff --git a/README.md b/README.md index 907b0cd..31039f1 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,50 @@ **Status:** Active — eBay listing intelligence MVP complete (search, trust scoring, affiliate links, feedback FAB, vision task scheduling). Auction sniping engine and multi-platform support are next. +## Quick install (self-hosted) + +**Requirements:** Docker with Compose plugin, Git. No API keys needed to get started. + +```bash +# One-line install — clones to ~/snipe by default +bash <(curl -fsSL https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/raw/branch/main/install.sh) + +# Or clone manually and run the script: +git clone https://git.opensourcesolarpunk.com/Circuit-Forge/snipe.git +bash snipe/install.sh +``` + +Then open **http://localhost:8509**. + +### Manual setup (if you prefer) + +Snipe's API image is built from a parent context that includes `circuitforge-core`. Both repos must sit as siblings in the same directory: + +``` +workspace/ +├── snipe/ ← this repo +└── circuitforge-core/ ← required sibling +``` + +```bash +mkdir snipe-workspace && cd snipe-workspace +git clone https://git.opensourcesolarpunk.com/Circuit-Forge/snipe.git +git clone https://git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core.git +cd snipe +cp .env.example .env # edit if you have eBay API credentials (optional) +./manage.sh start +``` + +### Optional: eBay API credentials + +Snipe works without any credentials using its Playwright scraper fallback. Adding eBay API credentials unlocks faster searches and inline seller account age (no extra scrape needed): + +1. Register at [developer.ebay.com](https://developer.ebay.com/my/keys) +2. Copy your Production **App ID** and **Cert ID** into `.env` +3. Restart: `./manage.sh restart` + +--- + ## What it does Snipe has two layers that work together: diff --git a/compose.override.yml b/compose.override.yml index 9a9805c..d1f0686 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -1,21 +1,17 @@ +# compose.override.yml — dev-only additions (auto-applied by Docker Compose in dev). +# Safe to delete on a self-hosted machine — compose.yml is self-contained. +# +# What this adds over compose.yml: +# - Live source mounts so code changes take effect without rebuilding images +# - RELOAD=true to enable uvicorn --reload for the API +# - NOTE: circuitforge-core is NOT mounted here — use `./manage.sh build` to +# pick up cf-core changes. Mounting it as a bind volume would break self-hosted +# installs that don't have the sibling directory. services: api: - build: - context: .. - dockerfile: snipe/Dockerfile - network_mode: host volumes: - - ../circuitforge-core:/app/circuitforge-core - ./api:/app/snipe/api - ./app:/app/snipe/app - - ./data:/app/snipe/data - ./tests:/app/snipe/tests environment: - RELOAD=true - - web: - build: - context: . - dockerfile: docker/web/Dockerfile - volumes: - - ./web/src:/app/src # not used at runtime but keeps override valid diff --git a/compose.yml b/compose.yml index b63d363..be84baf 100644 --- a/compose.yml +++ b/compose.yml @@ -3,11 +3,14 @@ services: build: context: .. dockerfile: snipe/Dockerfile - ports: - - "8510:8510" + # Host networking lets nginx (in the web container) reach the API at + # 172.17.0.1:8510 (the Docker bridge gateway). Required — nginx.conf + # is baked into the image and hard-codes that address. + network_mode: host env_file: .env volumes: - ./data:/app/snipe/data + restart: unless-stopped web: build: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7d4a2c6 --- /dev/null +++ b/install.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Snipe — self-hosted install script +# Clones Snipe and its shared library (circuitforge-core) into a workspace, +# then starts the Docker stack. +# +# Usage: +# bash install.sh # installs to ~/snipe +# bash install.sh /opt/snipe # custom install directory +# +# Requirements: Docker with Compose plugin, Git + +set -euo pipefail + +INSTALL_DIR="${1:-$HOME/snipe}" +FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge" + +info() { echo " [snipe] $*"; } +ok() { echo "✓ $*"; } +fail() { echo "✗ $*" >&2; exit 1; } + +echo "" +echo " Snipe — self-hosted installer" +echo " Install directory: $INSTALL_DIR" +echo "" + +# ── Pre-flight checks ──────────────────────────────────────────────────────── + +command -v docker >/dev/null 2>&1 || fail "Docker is required. Install from https://docs.docker.com/get-docker/" +docker compose version >/dev/null 2>&1 || fail "Docker Compose plugin is required (docker compose, not docker-compose)." +command -v git >/dev/null 2>&1 || fail "Git is required." +ok "Docker $(docker --version | awk '{print $3}' | tr -d ,) and Git found." + +# ── Clone repos ────────────────────────────────────────────────────────────── + +# compose.yml builds with context: .. so both repos must be siblings. +SNIPE_DIR="$INSTALL_DIR/snipe" +CORE_DIR="$INSTALL_DIR/circuitforge-core" + +if [[ -d "$SNIPE_DIR" ]]; then + info "Snipe already exists at $SNIPE_DIR — pulling latest..." + git -C "$SNIPE_DIR" pull --ff-only +else + info "Cloning Snipe..." + mkdir -p "$INSTALL_DIR" + git clone "$FORGEJO/snipe.git" "$SNIPE_DIR" +fi +ok "Snipe cloned to $SNIPE_DIR" + +if [[ -d "$CORE_DIR" ]]; then + info "circuitforge-core already exists — pulling latest..." + git -C "$CORE_DIR" pull --ff-only +else + info "Cloning circuitforge-core (shared library)..." + git clone "$FORGEJO/circuitforge-core.git" "$CORE_DIR" +fi +ok "circuitforge-core cloned to $CORE_DIR" + +# ── Configure environment ──────────────────────────────────────────────────── + +ENV_FILE="$SNIPE_DIR/.env" +if [[ ! -f "$ENV_FILE" ]]; then + cp "$SNIPE_DIR/.env.example" "$ENV_FILE" + ok ".env created from .env.example" + echo "" + echo " ┌────────────────────────────────────────────────────────┐" + echo " │ Next step: edit $ENV_FILE │" + echo " │ │" + echo " │ Snipe works out of the box with no API keys. │" + echo " │ Add EBAY_APP_ID / EBAY_CERT_ID for faster searches │" + echo " │ and full seller account age data (optional). │" + echo " └────────────────────────────────────────────────────────┘" + echo "" +else + info ".env already exists — skipping (delete it to reset)" +fi + +# ── Build and start ────────────────────────────────────────────────────────── + +info "Building Docker images (first run downloads ~1 GB of dependencies)..." +cd "$SNIPE_DIR" +docker compose build + +info "Starting Snipe..." +docker compose up -d + +echo "" +ok "Snipe is running!" +echo "" +echo " Web UI: http://localhost:8509" +echo " API: http://localhost:8510/docs" +echo "" +echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}" +echo "" diff --git a/manage.sh b/manage.sh index 58a517f..dc63332 100755 --- a/manage.sh +++ b/manage.sh @@ -78,7 +78,7 @@ case "$cmd" in test) echo "Running test suite..." docker compose -f "$COMPOSE_FILE" exec api \ - conda run -n job-seeker python -m pytest /app/snipe/tests/ -v "${@}" + python -m pytest /app/snipe/tests/ -v "${@}" ;; # ── Cloud commands ────────────────────────────────────────────────────────