feat: infra/devops batch — CI/CD, installer, nginx docs, cf-orch agent (v0.3.0)
Closes #15, #22, #24, #25. Closes #1 and #27 (already shipped in 0.2.0). ## CI/CD (#22) - .forgejo/workflows/ci.yml — Python lint (ruff) + pytest + Vue typecheck + vitest on every PR/push. Installs cf-core from GitHub mirror for the CI runner. - .forgejo/workflows/release.yml — Docker build/push (api + web) to Forgejo registry on v* tags; git-cliff changelog; multi-arch amd64+arm64. - .forgejo/workflows/mirror.yml — push to GitHub + Codeberg mirrors. ## Self-hosted installer (#25) - install.sh rewritten to match CF installer pattern: coloured output, named functions, --docker / --bare-metal / --help flags, auto-detect Docker/conda/ Python/Node/Chromium/Xvfb, license key prompting with format validation. ## Nginx docs (#24) - docs/nginx-self-hosted.conf — sample nginx config: SPA fallback, SSE proxy (proxy_buffering off), long-term asset cache headers. - docs/getting-started/installation.md — bare-metal install section with nginx setup, Chromium/Xvfb note, serve-ui.sh vs nginx trade-off. ## cf-orch agent (#15) - compose.override.yml — cf-orch-agent sidecar service (profiles: [orch]). Starts only with docker compose --profile orch. Registers with coordinator at CF_ORCH_COORDINATOR_URL (default 10.1.10.71:7700). - .env.example — CF_ORCH_URL / CF_ORCH_COORDINATOR_URL comments expanded. ## Docs - mkdocs.yml + full docs/ tree (getting-started, reference, user-guide) staged from prior session work. Bump version 0.2.0 → 0.3.0.
This commit is contained in:
parent
6d5ceac0a1
commit
2dda26a911
19 changed files with 1101 additions and 148 deletions
12
.env.example
12
.env.example
|
|
@ -72,9 +72,15 @@ SNIPE_DB=data/snipe.db
|
||||||
# OLLAMA_HOST=http://localhost:11434
|
# OLLAMA_HOST=http://localhost:11434
|
||||||
# OLLAMA_MODEL=llava:7b
|
# OLLAMA_MODEL=llava:7b
|
||||||
|
|
||||||
# CF Orchestrator — managed inference for Paid+ cloud users (internal use only).
|
# CF Orchestrator — routes vision/LLM tasks to a cf-orch coordinator for VRAM management.
|
||||||
# Self-hosted users leave this unset; it has no effect without a valid allocation token.
|
# Self-hosted: point at a local cf-orch coordinator if you have one running.
|
||||||
# CF_ORCH_URL=https://orch.circuitforge.tech
|
# Cloud (internal): managed coordinator at orch.circuitforge.tech.
|
||||||
|
# Leave unset to run vision tasks inline (no VRAM coordination).
|
||||||
|
# CF_ORCH_URL=http://10.1.10.71:7700
|
||||||
|
#
|
||||||
|
# cf-orch agent (compose --profile orch) — coordinator URL for the sidecar agent.
|
||||||
|
# Defaults to CF_ORCH_URL if unset.
|
||||||
|
# CF_ORCH_COORDINATOR_URL=http://10.1.10.71:7700
|
||||||
|
|
||||||
# ── In-app feedback (beta) ────────────────────────────────────────────────────
|
# ── In-app feedback (beta) ────────────────────────────────────────────────────
|
||||||
# When set, a feedback FAB appears in the UI and routes submissions to Forgejo.
|
# When set, a feedback FAB appears in the UI and routes submissions to Forgejo.
|
||||||
|
|
|
||||||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -6,6 +6,36 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.3.0] — 2026-04-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
**Infrastructure and DevOps**
|
||||||
|
|
||||||
|
- `.forgejo/workflows/ci.yml` — Python lint (ruff) + pytest + Vue typecheck + vitest on every PR/push to main. Installs circuitforge-core from GitHub mirror so the CI runner doesn't need the sibling directory.
|
||||||
|
- `.forgejo/workflows/release.yml` — Docker build and push (api + web images) to Forgejo container registry on `v*` tags. Builds both images multi-arch (amd64 + arm64). Creates a Forgejo release with git-cliff changelog notes.
|
||||||
|
- `.forgejo/workflows/mirror.yml` — Mirror push to GitHub and Codeberg on main/tags.
|
||||||
|
- `install.sh` — Full rewrite following the CircuitForge installer pattern: colored output, `--docker` / `--bare-metal` / `--help` flags, auto-detection of Docker/conda/Python/Node/Chromium/Xvfb, license key prompting, structured named functions.
|
||||||
|
- `docs/nginx-self-hosted.conf` — Sample nginx config for bare-metal self-hosted deployments (SPA fallback, SSE proxy settings, long-term asset caching).
|
||||||
|
- `docs/getting-started/installation.md` — No-Docker install section: bare-metal instructions, nginx setup, Chromium/Xvfb note.
|
||||||
|
- `compose.override.yml` — `cf-orch-agent` sidecar service for routing vision tasks to a cf-orch GPU coordinator (`--profile orch` opt-in). `CF_ORCH_COORDINATOR_URL` env var documented.
|
||||||
|
- `.env.example` — `CF_ORCH_URL` and `CF_ORCH_COORDINATOR_URL` comments expanded with self-hosted coordinator guidance.
|
||||||
|
|
||||||
|
**Screenshots** (post CSS fix)
|
||||||
|
|
||||||
|
- Retook all docs screenshots (`01-hero`, `02-results`, `03-steal-badge`, `hero`) after the color-mix token fix so tints match the theme in both dark and light mode.
|
||||||
|
|
||||||
|
### Closed
|
||||||
|
|
||||||
|
- `#1` SSE live score push — already fully implemented in 0.2.0; closed.
|
||||||
|
- `#22` Forgejo Actions CI/CD — shipped.
|
||||||
|
- `#24` nginx config for no-Docker self-hosting — shipped.
|
||||||
|
- `#25` Self-hosted installer script — shipped.
|
||||||
|
- `#15` cf-orch agent in compose stack — shipped.
|
||||||
|
- `#27` MCP server — already shipped in 0.2.0; closed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.2.0] — 2026-04-12
|
## [0.2.0] — 2026-04-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
# What this adds over compose.yml:
|
# What this adds over compose.yml:
|
||||||
# - Live source mounts so code changes take effect without rebuilding images
|
# - Live source mounts so code changes take effect without rebuilding images
|
||||||
# - RELOAD=true to enable uvicorn --reload for the API
|
# - RELOAD=true to enable uvicorn --reload for the API
|
||||||
# - NOTE: circuitforge-core is NOT mounted here — use `./manage.sh build` to
|
# - cf-orch-agent sidecar for local GPU task routing (opt-in: --profile orch)
|
||||||
|
#
|
||||||
|
# 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
|
# pick up cf-core changes. Mounting it as a bind volume would break self-hosted
|
||||||
# installs that don't have the sibling directory.
|
# installs that don't have the sibling directory.
|
||||||
services:
|
services:
|
||||||
|
|
@ -15,3 +17,32 @@ services:
|
||||||
- ./tests:/app/snipe/tests
|
- ./tests:/app/snipe/tests
|
||||||
environment:
|
environment:
|
||||||
- RELOAD=true
|
- RELOAD=true
|
||||||
|
# Point the LLM/vision task scheduler at the local cf-orch coordinator.
|
||||||
|
# Only has effect when CF_ORCH_URL is set (uncomment in .env, or set inline).
|
||||||
|
# - CF_ORCH_URL=http://10.1.10.71:7700
|
||||||
|
|
||||||
|
# cf-orch agent — routes trust_photo_analysis vision tasks to the GPU coordinator.
|
||||||
|
# Only starts when you pass --profile orch:
|
||||||
|
# docker compose --profile orch up
|
||||||
|
#
|
||||||
|
# Requires a running cf-orch coordinator. Default: Heimdall at 10.1.10.71:7700.
|
||||||
|
# Override via CF_ORCH_COORDINATOR_URL in .env.
|
||||||
|
#
|
||||||
|
# To use a locally-built cf-orch image instead of the published one:
|
||||||
|
# build:
|
||||||
|
# context: ../circuitforge-orch
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
cf-orch-agent:
|
||||||
|
image: ghcr.io/circuitforgellc/cf-orch:latest
|
||||||
|
command: >
|
||||||
|
agent
|
||||||
|
--coordinator ${CF_ORCH_COORDINATOR_URL:-http://10.1.10.71:7700}
|
||||||
|
--node-id snipe-dev
|
||||||
|
--host 0.0.0.0
|
||||||
|
--port 7701
|
||||||
|
--advertise-host 127.0.0.1
|
||||||
|
environment:
|
||||||
|
CF_COORDINATOR_URL: ${CF_ORCH_COORDINATOR_URL:-http://10.1.10.71:7700}
|
||||||
|
restart: on-failure
|
||||||
|
profiles:
|
||||||
|
- orch
|
||||||
|
|
|
||||||
39
docs/getting-started/ebay-api.md
Normal file
39
docs/getting-started/ebay-api.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# eBay API Keys (Optional)
|
||||||
|
|
||||||
|
Snipe works without any credentials using its Playwright scraper fallback. Adding eBay API credentials unlocks faster searches and higher rate limits.
|
||||||
|
|
||||||
|
## What API keys enable
|
||||||
|
|
||||||
|
| Feature | Without keys | With keys |
|
||||||
|
|---------|-------------|-----------|
|
||||||
|
| Listing search | Playwright scraper | eBay Browse API (faster, higher limits) |
|
||||||
|
| Market comps (completed sales) | Not available | eBay Marketplace Insights API |
|
||||||
|
| Seller account data | BTF scraper (Xvfb) | BTF scraper (same — eBay API doesn't expose join date) |
|
||||||
|
|
||||||
|
## Getting credentials
|
||||||
|
|
||||||
|
1. Create a developer account at [developer.ebay.com](https://developer.ebay.com/my/keys)
|
||||||
|
2. Create a new application (choose **Production**)
|
||||||
|
3. Copy your **App ID (Client ID)** and **Cert ID (Client Secret)**
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add your credentials to `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EBAY_APP_ID=YourAppID-...
|
||||||
|
EBAY_CERT_ID=YourCertID-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./manage.sh restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying
|
||||||
|
|
||||||
|
After restart, the search bar shows **API** as available in the data source selector. The auto mode will use the API by default.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
The Marketplace Insights API (for completed sales comps) requires an approved eBay developer account. New accounts may not have access. Snipe gracefully falls back to Browse API results when Insights returns 403 or 404.
|
||||||
102
docs/getting-started/installation.md
Normal file
102
docs/getting-started/installation.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker with Compose plugin
|
||||||
|
- Git
|
||||||
|
- No API keys required to get started
|
||||||
|
|
||||||
|
## One-line install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/raw/branch/main/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
This clones the repo to `~/snipe` and starts the stack. Open **http://localhost:8509** when it completes.
|
||||||
|
|
||||||
|
## Manual install
|
||||||
|
|
||||||
|
Snipe's API image is built from a context that includes `circuitforge-core`. Both repos must sit as siblings:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
./manage.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Managing the stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./manage.sh start # build and start all containers
|
||||||
|
./manage.sh stop # stop containers
|
||||||
|
./manage.sh restart # rebuild and restart
|
||||||
|
./manage.sh status # container health
|
||||||
|
./manage.sh logs # tail logs
|
||||||
|
./manage.sh open # open in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
./manage.sh restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
| Service | Default port |
|
||||||
|
|---------|-------------|
|
||||||
|
| Web UI | 8509 |
|
||||||
|
| API | 8510 |
|
||||||
|
|
||||||
|
Both ports are configurable in `.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No-Docker install (bare metal)
|
||||||
|
|
||||||
|
Run `install.sh --bare-metal` to skip Docker and install via conda or venv instead.
|
||||||
|
This sets up the Python environment, builds the Vue frontend, and writes helper scripts.
|
||||||
|
|
||||||
|
**Requirements:** Python 3.11+, Node.js 20+, `xvfb` (for the eBay scraper).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -fsSL https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/raw/branch/main/install.sh) --bare-metal
|
||||||
|
```
|
||||||
|
|
||||||
|
After install, you get two scripts:
|
||||||
|
|
||||||
|
| Script | What it does |
|
||||||
|
|--------|-------------|
|
||||||
|
| `./start-local.sh` | Start the FastAPI API on port 8510 |
|
||||||
|
| `./serve-ui.sh` | Serve the built frontend with `python3 -m http.server 8509` (dev only) |
|
||||||
|
|
||||||
|
`serve-ui.sh` is single-threaded and suitable for testing only. For a real deployment, use nginx.
|
||||||
|
|
||||||
|
### nginx config (production bare-metal)
|
||||||
|
|
||||||
|
Install nginx, copy the sample config, and reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp docs/nginx-self-hosted.conf /etc/nginx/sites-available/snipe
|
||||||
|
sudo ln -s /etc/nginx/sites-available/snipe /etc/nginx/sites-enabled/snipe
|
||||||
|
# Edit the file — update `root` to your actual web/dist path
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`docs/nginx-self-hosted.conf`](../nginx-self-hosted.conf) for the full config with TLS notes.
|
||||||
|
|
||||||
|
### Chromium / Xvfb note
|
||||||
|
|
||||||
|
Snipe uses headed Chromium via Xvfb to bypass Kasada (the anti-bot layer on eBay seller profile pages). If Chromium is not detected, the scraper falls back to the eBay Browse API — add `EBAY_APP_ID` / `EBAY_CERT_ID` to `.env` so that fallback has credentials.
|
||||||
|
|
||||||
|
The installer detects and installs Xvfb automatically on Debian/Ubuntu/Fedora. Chromium is installed via `playwright install chromium`. macOS is not supported for the scraper path.
|
||||||
39
docs/getting-started/quick-start.md
Normal file
39
docs/getting-started/quick-start.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
## 1. Run a search
|
||||||
|
|
||||||
|
Type a query into the search bar and press **Search** or hit Enter.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
Start broad (`vintage camera`) then narrow with keyword filters once you see results. The must-include and must-exclude fields let you refine without re-searching from scratch.
|
||||||
|
|
||||||
|
## 2. Read the trust badge
|
||||||
|
|
||||||
|
Each listing card shows a trust badge in the top-right corner:
|
||||||
|
|
||||||
|
| Badge | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| Green (70–100) | Established seller, no major concerns |
|
||||||
|
| Yellow (40–69) | Some signals missing or marginal |
|
||||||
|
| Red (0–39) | Multiple red flags — proceed carefully |
|
||||||
|
| `STEAL` label | Price significantly below market median |
|
||||||
|
|
||||||
|
A spinning indicator below the badge means enrichment is still in progress (account age is being fetched). Scores update automatically when enrichment completes.
|
||||||
|
|
||||||
|
## 3. Check red flags
|
||||||
|
|
||||||
|
Red flag pills appear below the listing title when Snipe detects a concern. Hover or tap a flag for a plain-language explanation.
|
||||||
|
|
||||||
|
## 4. Click through to eBay
|
||||||
|
|
||||||
|
Listing titles link directly to eBay. In cloud mode, links include an affiliate code that supports Snipe's development at no cost to you. You can opt out in Settings.
|
||||||
|
|
||||||
|
## 5. Filter results
|
||||||
|
|
||||||
|
Use the sidebar filters to narrow results without re-running the eBay search:
|
||||||
|
|
||||||
|
- **Min trust score** — slider to hide low-confidence listings
|
||||||
|
- **Min account age / Min feedback** — hide new or low-volume sellers
|
||||||
|
- **Hide listings checkboxes** — hide new accounts, suspicious prices, duplicate photos, damage mentions, long-on-market, significant price drop
|
||||||
|
|
||||||
|
These filters apply instantly to the current result set. Use the search bar to change the underlying eBay query.
|
||||||
33
docs/index.md
Normal file
33
docs/index.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Snipe
|
||||||
|
|
||||||
|
**eBay trust scoring before you bid.**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Snipe scores eBay listings and sellers for trustworthiness before you place a bid. Paste a search query, get results with trust scores, and know exactly which listings are worth your time.
|
||||||
|
|
||||||
|
## What it catches
|
||||||
|
|
||||||
|
- **New accounts** selling high-value items with no track record
|
||||||
|
- **Suspicious prices** — listings priced far below completed sales
|
||||||
|
- **Duplicate photos** — images copy-pasted from other listings (perceptual hash deduplication)
|
||||||
|
- **Damage buried in titles** — scratch, dent, untested, for parts, and similar
|
||||||
|
- **Known bad actors** — sellers on the community blocklist
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Each listing gets a composite trust score from 0–100 based on five seller signals: account age, feedback count, feedback ratio, price vs. market, and category history. Red flags are surfaced alongside the score, not buried in it.
|
||||||
|
|
||||||
|
## Free, no account required
|
||||||
|
|
||||||
|
Search and scoring work without creating an account. Community features (reporting sellers, importing blocklists) require a free account.
|
||||||
|
|
||||||
|
## Quick links
|
||||||
|
|
||||||
|
- [Installation](getting-started/installation.md)
|
||||||
|
- [Understanding trust scores](user-guide/trust-scores.md)
|
||||||
|
- [Red flags reference](user-guide/red-flags.md)
|
||||||
|
- [Cloud demo](https://menagerie.circuitforge.tech/snipe)
|
||||||
|
- [Source code](https://git.opensourcesolarpunk.com/Circuit-Forge/snipe)
|
||||||
58
docs/nginx-self-hosted.conf
Normal file
58
docs/nginx-self-hosted.conf
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# nginx config for Snipe — bare-metal self-hosted (no Docker).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# sudo cp docs/nginx-self-hosted.conf /etc/nginx/sites-available/snipe
|
||||||
|
# # Edit: update `root` to your actual web/dist path and `server_name` to your hostname
|
||||||
|
# sudo ln -s /etc/nginx/sites-available/snipe /etc/nginx/sites-enabled/snipe
|
||||||
|
# sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
#
|
||||||
|
# Assumes:
|
||||||
|
# - The Snipe FastAPI API is running on 127.0.0.1:8510 (./start-local.sh)
|
||||||
|
# - The Vue frontend was built by install.sh into web/dist/
|
||||||
|
# - TLS termination is handled separately (Caddy, certbot, or upstream proxy)
|
||||||
|
#
|
||||||
|
# For TLS with Let's Encrypt, run:
|
||||||
|
# sudo certbot --nginx -d your.domain.com
|
||||||
|
# Certbot will add the ssl_certificate lines automatically.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your.domain.com; # replace or use _ for catch-all
|
||||||
|
|
||||||
|
# Path to the Vue production build — update to match your install directory
|
||||||
|
root /home/youruser/snipe/snipe/web/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Proxy all /api/ requests to the FastAPI backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8510;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# SSE (Server-Sent Events) — live trust score updates
|
||||||
|
# These are long-lived streaming responses; disable buffering.
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# index.html — never cache; ensures clients always get the latest entry point
|
||||||
|
# after a deployment (JS/CSS chunks are content-hashed so they cache forever)
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA fallback — all unknown paths serve index.html so Vue Router handles routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long-term cache for content-hashed static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
66
docs/reference/architecture.md
Normal file
66
docs/reference/architecture.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Frontend | Vue 3, Pinia, UnoCSS |
|
||||||
|
| API | FastAPI (Python), host networking |
|
||||||
|
| Database | SQLite (WAL mode) |
|
||||||
|
| Scraper | Playwright + Chromium + Xvfb |
|
||||||
|
| Container | Docker Compose |
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
User -->|search query| VueSPA
|
||||||
|
VueSPA -->|GET /api/search| FastAPI
|
||||||
|
FastAPI -->|Browse API or Playwright| eBay
|
||||||
|
eBay --> FastAPI
|
||||||
|
FastAPI -->|score_batch| TrustScorer
|
||||||
|
TrustScorer --> FastAPI
|
||||||
|
FastAPI -->|BTF enrich queue| XvfbScraper
|
||||||
|
XvfbScraper -->|seller profile| eBayProfile
|
||||||
|
eBayProfile --> XvfbScraper
|
||||||
|
XvfbScraper -->|account_age update| SQLite
|
||||||
|
FastAPI -->|SSE push| VueSPA
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database layout
|
||||||
|
|
||||||
|
Snipe uses two SQLite databases in cloud mode:
|
||||||
|
|
||||||
|
| Database | Contents |
|
||||||
|
|----------|---------|
|
||||||
|
| `shared.db` | Sellers, listings, market comps, community signals, scammer blocklist |
|
||||||
|
| `user.db` | Trust scores, saved searches, user preferences, background tasks |
|
||||||
|
|
||||||
|
In local (self-hosted) mode, everything uses a single `snipe.db`.
|
||||||
|
|
||||||
|
WAL (Write-Ahead Logging) mode is enabled on all connections for concurrent reader safety.
|
||||||
|
|
||||||
|
## Seller enrichment pipeline
|
||||||
|
|
||||||
|
eBay's Browse API returns listings without seller account ages. Snipe fetches account ages by loading the seller's eBay profile page in a headed Chromium instance via Xvfb.
|
||||||
|
|
||||||
|
Each enrichment session uses a unique Xvfb display number (`:200`–`:299`, cycling) to prevent lock file collisions across parallel sessions. Kasada bot protection blocks headless Chrome and curl-based requests — only a full headed browser session passes.
|
||||||
|
|
||||||
|
## Affiliate URL wrapping
|
||||||
|
|
||||||
|
All listing URLs are wrapped with an eBay Partner Network (EPN) affiliate code before being returned to the frontend. Resolution order:
|
||||||
|
|
||||||
|
1. User opted out → plain URL
|
||||||
|
2. User has BYOK EPN ID (Premium) → wrap with user's ID
|
||||||
|
3. CF affiliate ID configured in `.env` → wrap with CF's ID
|
||||||
|
4. Not configured → plain URL
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
| Layer | License |
|
||||||
|
|-------|---------|
|
||||||
|
| Discovery pipeline (scraper, trust scoring, search) | MIT |
|
||||||
|
| AI features (photo analysis, description reasoning) | BSL 1.1 |
|
||||||
|
| Fine-tuned model weights | Proprietary |
|
||||||
|
|
||||||
|
BSL 1.1 is free for personal non-commercial self-hosting. SaaS re-hosting requires a commercial license. Converts to MIT after 4 years.
|
||||||
32
docs/reference/tier-system.md
Normal file
32
docs/reference/tier-system.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Tier System
|
||||||
|
|
||||||
|
Snipe uses Circuit Forge's standard four-tier model.
|
||||||
|
|
||||||
|
## Tiers
|
||||||
|
|
||||||
|
| Tier | Price | Key features |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| **Free** | Free | Search, trust scoring, red flags, blocklist, market comps, affiliate links, saved searches |
|
||||||
|
| **Paid** | $4.99/mo or $129 lifetime | Photo analysis, background monitoring (up to 5 searches), serial number check |
|
||||||
|
| **Premium** | $9.99/mo or $249 lifetime | All Paid features, background monitoring (up to 25), custom affiliate ID (BYOK EPN) |
|
||||||
|
| **Ultra** | Contact us | Human-in-the-loop assistance |
|
||||||
|
|
||||||
|
## Free tier philosophy
|
||||||
|
|
||||||
|
Snipe's core trust-scoring pipeline — the part that actually catches scammers — is entirely free and requires no account. This is intentional.
|
||||||
|
|
||||||
|
More users = more community blocklist data = better protection for everyone. The free tier drives the network effect that makes the paid features more valuable.
|
||||||
|
|
||||||
|
## Self-hosted
|
||||||
|
|
||||||
|
Running Snipe yourself? All features are available with no tier gates in local mode. Bring your own LLM (Ollama compatible) to unlock photo analysis and description reasoning on your own hardware.
|
||||||
|
|
||||||
|
## BYOK (Bring Your Own Key)
|
||||||
|
|
||||||
|
Premium subscribers can supply:
|
||||||
|
- **Local LLM endpoint** — any OpenAI-compatible server (Ollama, vLLM, LM Studio) unlocks AI features on Free tier
|
||||||
|
- **eBay Partner Network campaign ID** — your affiliate revenue instead of Snipe's
|
||||||
|
|
||||||
|
## Cloud trial
|
||||||
|
|
||||||
|
15-day free trial of Paid tier on first signup. No credit card required.
|
||||||
84
docs/reference/trust-scoring.md
Normal file
84
docs/reference/trust-scoring.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Trust Score Algorithm
|
||||||
|
|
||||||
|
## Signal scoring
|
||||||
|
|
||||||
|
Each signal contributes 0–20 points to the composite score.
|
||||||
|
|
||||||
|
### account_age
|
||||||
|
|
||||||
|
| Days old | Score |
|
||||||
|
|----------|-------|
|
||||||
|
| < 7 | 0 (triggers `new_account` hard flag) |
|
||||||
|
| 7–30 | 5 |
|
||||||
|
| 30–90 | 10 |
|
||||||
|
| 90–365 | 15 |
|
||||||
|
| > 365 | 20 |
|
||||||
|
|
||||||
|
Data source: eBay profile page (BTF scraper via headed Chromium + Xvfb — eBay API does not expose account registration date).
|
||||||
|
|
||||||
|
### feedback_count
|
||||||
|
|
||||||
|
| Count | Score |
|
||||||
|
|-------|-------|
|
||||||
|
| 0 | 0 (triggers `zero_feedback` hard flag, score capped at 35) |
|
||||||
|
| 1–9 | 5 |
|
||||||
|
| 10–49 | 10 |
|
||||||
|
| 50–199 | 15 |
|
||||||
|
| 200+ | 20 |
|
||||||
|
|
||||||
|
### feedback_ratio
|
||||||
|
|
||||||
|
| Ratio | Score |
|
||||||
|
|-------|-------|
|
||||||
|
| < 80% (with 20+ reviews) | 0 (triggers `established_bad_actor`) |
|
||||||
|
| < 90% | 5 |
|
||||||
|
| 90–94% | 10 |
|
||||||
|
| 95–98% | 15 |
|
||||||
|
| 99–100% | 20 |
|
||||||
|
|
||||||
|
### price_vs_market
|
||||||
|
|
||||||
|
Compares listing price to the median of recent completed sales from eBay Marketplace Insights API.
|
||||||
|
|
||||||
|
| Price vs. median | Score |
|
||||||
|
|-----------------|-------|
|
||||||
|
| < 40% | 0 (triggers `suspicious_price` flag) |
|
||||||
|
| 40–59% | 5 |
|
||||||
|
| 60–79% | 10 |
|
||||||
|
| 80–120% | 20 (normal range) |
|
||||||
|
| 121–149% | 15 |
|
||||||
|
| 150%+ | 10 |
|
||||||
|
|
||||||
|
`suspicious_price` flag is suppressed when the market price distribution is too wide (standard deviation > 50% of median) — this prevents false positives on heterogeneous search results.
|
||||||
|
|
||||||
|
When no market data is available, this signal returns `None` and is excluded from the composite.
|
||||||
|
|
||||||
|
### category_history
|
||||||
|
|
||||||
|
Derived from the seller's recent listing history (categories of their sold items):
|
||||||
|
|
||||||
|
| Result | Score |
|
||||||
|
|--------|-------|
|
||||||
|
| Seller has history in this category | 20 |
|
||||||
|
| Seller sells cross-category (generalist) | 10 |
|
||||||
|
| No category history available | None (excluded from composite) |
|
||||||
|
|
||||||
|
## Composite calculation
|
||||||
|
|
||||||
|
```
|
||||||
|
composite = (sum of available signal scores) / (20 × count of available signals) × 100
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures missing signals don't penalize a seller — only available signals count toward the denominator.
|
||||||
|
|
||||||
|
## Zero-feedback cap
|
||||||
|
|
||||||
|
When `feedback_count == 0`, the composite is hard-capped at **35** after the standard calculation. A 0-feedback seller cannot score above 35 regardless of other signals.
|
||||||
|
|
||||||
|
## Partial scores
|
||||||
|
|
||||||
|
A score is marked **partial** when one or more signals are `None` (not yet available). The score is recalculated and the partial flag is cleared when enrichment completes.
|
||||||
|
|
||||||
|
## Red flag override
|
||||||
|
|
||||||
|
Red flags are evaluated independently of the composite score. A seller can have a high composite score and still trigger red flags — for example, a long-established seller with a suspicious-priced listing and duplicate photos.
|
||||||
34
docs/user-guide/blocklist.md
Normal file
34
docs/user-guide/blocklist.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Community Blocklist
|
||||||
|
|
||||||
|
The blocklist is a shared database of sellers flagged by Snipe users. When a blocklisted seller appears in search results, their listing card is marked with an `established_bad_actor` flag.
|
||||||
|
|
||||||
|
## Viewing the blocklist
|
||||||
|
|
||||||
|
Navigate to **Blocklist** in the sidebar to see all reported sellers, with usernames, platforms, and optional reasons.
|
||||||
|
|
||||||
|
## Reporting a seller
|
||||||
|
|
||||||
|
On any listing card, click the **Block** button (shield icon) to report the seller. You can optionally add a reason (e.g. "sent counterfeit item", "never shipped").
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
In cloud mode, blocking requires a signed-in account. Anonymous users can view the blocklist but cannot report sellers.
|
||||||
|
|
||||||
|
## Importing a blocklist
|
||||||
|
|
||||||
|
The Blocklist view has an **Import CSV** button. The accepted format:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
platform,platform_seller_id,username,reason
|
||||||
|
ebay,seller123,seller123,counterfeit item
|
||||||
|
ebay,badactor99,badactor99,
|
||||||
|
```
|
||||||
|
|
||||||
|
The `reason` column is optional. `platform` defaults to `ebay` if omitted.
|
||||||
|
|
||||||
|
## Exporting the blocklist
|
||||||
|
|
||||||
|
Click **Export CSV** in the Blocklist view to download the current blocklist. Use this to back up, share with others, or import into another Snipe instance.
|
||||||
|
|
||||||
|
## Blocklist sync (roadmap)
|
||||||
|
|
||||||
|
Batch reporting to eBay's Trust & Safety team is on the roadmap (issue #4). This would allow community-flagged sellers to be reported directly to eBay from within Snipe.
|
||||||
58
docs/user-guide/red-flags.md
Normal file
58
docs/user-guide/red-flags.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Red Flags
|
||||||
|
|
||||||
|
Red flags appear as pills on listing cards when Snipe detects a concern. Each flag is independent — a listing can have multiple flags at once.
|
||||||
|
|
||||||
|
## Hard red flags
|
||||||
|
|
||||||
|
These override the composite score display with a strong visual warning.
|
||||||
|
|
||||||
|
### `zero_feedback`
|
||||||
|
Seller has received zero feedback. Score is capped at 35.
|
||||||
|
|
||||||
|
### `new_account`
|
||||||
|
Account registered within the last 7 days. Extremely high fraud indicator for high-value listings.
|
||||||
|
|
||||||
|
### `established_bad_actor`
|
||||||
|
Feedback ratio below 80% with 20 or more reviews. A sustained pattern of negative feedback from an established seller.
|
||||||
|
|
||||||
|
## Soft flags
|
||||||
|
|
||||||
|
Shown as warnings — not automatic disqualifiers, but worth investigating.
|
||||||
|
|
||||||
|
### `account_under_30_days`
|
||||||
|
Account is less than 30 days old. Less severe than `new_account` but worth noting for high-value items.
|
||||||
|
|
||||||
|
### `low_feedback_count`
|
||||||
|
Fewer than 10 feedback ratings total. Seller is new to eBay or rarely transacts.
|
||||||
|
|
||||||
|
### `suspicious_price`
|
||||||
|
Listing price is more than 50% below the market median from recent completed sales.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This flag is suppressed automatically when the search returns a heterogeneous price range — for example, a search that mixes laptop generations spanning $200–$2,000. In that case, the median is not meaningful and flagging would produce false positives.
|
||||||
|
|
||||||
|
### `duplicate_photo`
|
||||||
|
The same image (by perceptual hash) appears on another listing. Common in scams where photos are lifted from legitimate listings.
|
||||||
|
|
||||||
|
### `scratch_dent_mentioned`
|
||||||
|
The title or description contains keywords indicating cosmetic damage, functional problems, or evasive language:
|
||||||
|
|
||||||
|
- Damage: *scratch, dent, crack, chip, broken, damaged*
|
||||||
|
- Functional: *untested, for parts, parts only, as-is, not working*
|
||||||
|
- Evasive: *read description, see description, sold as-is*
|
||||||
|
|
||||||
|
### `long_on_market`
|
||||||
|
The listing has been seen 5 or more times over 14 or more days without selling. A listing that isn't moving may be overpriced or have undisclosed problems.
|
||||||
|
|
||||||
|
### `significant_price_drop`
|
||||||
|
The current price is more than 20% below the price when Snipe first saw this listing. Sudden drops can indicate seller desperation — or a motivated seller — depending on context.
|
||||||
|
|
||||||
|
## Triple Red
|
||||||
|
|
||||||
|
When a listing hits all three of these simultaneously:
|
||||||
|
|
||||||
|
- `new_account` OR `account_under_30_days`
|
||||||
|
- `suspicious_price`
|
||||||
|
- `duplicate_photo` OR `zero_feedback` OR `established_bad_actor` OR `scratch_dent_mentioned`
|
||||||
|
|
||||||
|
The card gets a **pulsing red border glow** to make it impossible to miss in a crowded results grid.
|
||||||
56
docs/user-guide/searching.md
Normal file
56
docs/user-guide/searching.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Searching
|
||||||
|
|
||||||
|
## Basic search
|
||||||
|
|
||||||
|
Type a query and press **Search**. Snipe fetches listings from eBay and scores each seller in parallel.
|
||||||
|
|
||||||
|
Result count depends on the **Pages to fetch** setting (1 page = up to 200 listings). More pages means a more complete picture but a longer wait.
|
||||||
|
|
||||||
|
## Keyword modes
|
||||||
|
|
||||||
|
The must-include field has three modes:
|
||||||
|
|
||||||
|
| Mode | Behavior |
|
||||||
|
|------|---------|
|
||||||
|
| **All** | Every term must appear in results (eBay AND search) |
|
||||||
|
| **Any** | At least one term must appear (eBay OR search) |
|
||||||
|
| **Groups** | Comma-separated groups, each searched separately and merged |
|
||||||
|
|
||||||
|
Groups mode is the most powerful. Use it to search for variations that eBay's relevance ranking might drop:
|
||||||
|
|
||||||
|
```
|
||||||
|
16gb, 32gb
|
||||||
|
RTX 4090, 4090 founders
|
||||||
|
```
|
||||||
|
|
||||||
|
This sends two separate eBay queries and deduplicates the results by listing ID.
|
||||||
|
|
||||||
|
## Must-exclude
|
||||||
|
|
||||||
|
Terms in the must-exclude field are forwarded to eBay on re-search. Common uses:
|
||||||
|
|
||||||
|
```
|
||||||
|
broken, parts only, for parts, untested, cracked
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Must-exclude applies on re-search (it goes to eBay). The **Hide listings: Scratch/dent mentioned** sidebar filter applies instantly to current results using Snipe's own detection logic, which is more comprehensive than eBay's keyword exclusion.
|
||||||
|
|
||||||
|
## Filters sidebar
|
||||||
|
|
||||||
|
The sidebar has two sections:
|
||||||
|
|
||||||
|
**eBay Search** — settings forwarded to eBay on re-search:
|
||||||
|
- Category filter
|
||||||
|
- Price range (min/max)
|
||||||
|
- Pages to fetch
|
||||||
|
- Data source (Auto / API / Scraper)
|
||||||
|
|
||||||
|
**Filter Results** — applied instantly to current results:
|
||||||
|
- Min trust score slider
|
||||||
|
- Min account age / Min feedback count
|
||||||
|
- Hide listings checkboxes
|
||||||
|
|
||||||
|
## Saved searches
|
||||||
|
|
||||||
|
Click the bookmark icon next to the Search button to save a search with its current filter settings. Saved searches appear in the **Saved** view and can be re-run with one click, restoring all filters.
|
||||||
25
docs/user-guide/settings.md
Normal file
25
docs/user-guide/settings.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Settings
|
||||||
|
|
||||||
|
Navigate to **Settings** in the sidebar to access preferences.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
### Trust score feedback
|
||||||
|
|
||||||
|
Shows "This score looks right / wrong" buttons on each listing card. Your feedback is recorded anonymously and used to improve trust scoring for all users.
|
||||||
|
|
||||||
|
This is opt-in and enabled by default.
|
||||||
|
|
||||||
|
## Affiliate Links (cloud accounts only)
|
||||||
|
|
||||||
|
### Opt out of affiliate links
|
||||||
|
|
||||||
|
When enabled, listing links go directly to eBay without an affiliate code. Your purchases won't generate revenue for Snipe's development.
|
||||||
|
|
||||||
|
By default, Snipe includes an affiliate code in eBay links at no cost to you — you pay the same price either way.
|
||||||
|
|
||||||
|
### Custom affiliate ID (Premium)
|
||||||
|
|
||||||
|
Premium subscribers can supply their own eBay Partner Network (EPN) campaign ID. When set, your eBay purchases through Snipe links generate revenue for your own EPN account instead of Snipe's.
|
||||||
|
|
||||||
|
This requires an active EPN account at [partnernetwork.ebay.com](https://partnernetwork.ebay.com).
|
||||||
39
docs/user-guide/trust-scores.md
Normal file
39
docs/user-guide/trust-scores.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Trust Scores
|
||||||
|
|
||||||
|
## How scoring works
|
||||||
|
|
||||||
|
Each listing gets a composite trust score from 0–100, built from five signals:
|
||||||
|
|
||||||
|
| Signal | Max points | What it measures |
|
||||||
|
|--------|-----------|-----------------|
|
||||||
|
| `account_age` | 20 | Days since the seller's eBay account was registered |
|
||||||
|
| `feedback_count` | 20 | Total feedback received (volume proxy for experience) |
|
||||||
|
| `feedback_ratio` | 20 | Percentage of positive feedback |
|
||||||
|
| `price_vs_market` | 20 | How the listing price compares to recent completed sales |
|
||||||
|
| `category_history` | 20 | Whether the seller has a history in this item category |
|
||||||
|
|
||||||
|
The composite score is the sum of available signals divided by the maximum possible from available signals. Missing signals don't penalize the seller — they reduce the max rather than adding a zero.
|
||||||
|
|
||||||
|
## Score bands
|
||||||
|
|
||||||
|
| Score | Label | Meaning |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| 70–100 | Green | Established seller, no major concerns |
|
||||||
|
| 40–69 | Yellow | Some signals marginal or missing |
|
||||||
|
| 0–39 | Red | Multiple red flags — proceed carefully |
|
||||||
|
|
||||||
|
## Zero-feedback cap
|
||||||
|
|
||||||
|
A seller with zero feedback is hard-capped at a composite score of **35**, regardless of other signals. Zero feedback is the single strongest indicator of a fraudulent or new account, and it would be misleading to allow such a seller to score higher based on price alignment alone.
|
||||||
|
|
||||||
|
## Partial scores
|
||||||
|
|
||||||
|
When account age hasn't yet been enriched (the BTF scraper is still running), the score is marked **partial** and shown with a spinning indicator. Partial scores are based on available signals only and update automatically when enrichment completes — typically within 30–60 seconds per seller.
|
||||||
|
|
||||||
|
## STEAL badge
|
||||||
|
|
||||||
|
The **STEAL** badge appears when a listing's price is significantly below the market median from recently completed sales. This is a useful signal for buyers, but it can also indicate a scam — always cross-reference with the trust score and red flags.
|
||||||
|
|
||||||
|
## Market comps
|
||||||
|
|
||||||
|
Market price data comes from eBay's Marketplace Insights API (completed sales). When this API is unavailable (requires an approved eBay developer account), Snipe falls back to listing prices from the Browse API, which is less accurate. The market price shown in search results reflects whichever source was available.
|
||||||
434
install.sh
434
install.sh
|
|
@ -1,226 +1,384 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Snipe — self-hosted install script
|
# Snipe — self-hosted installer
|
||||||
#
|
#
|
||||||
# Supports two install paths:
|
# Supports two install paths:
|
||||||
# Docker (recommended) — everything in containers, no system Python deps required
|
# Docker (recommended) — everything in containers, no system Python deps required
|
||||||
# No-Docker — conda or venv + direct uvicorn, for machines without Docker
|
# Bare metal — conda or pip venv + uvicorn, for machines without Docker
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash install.sh # installs to ~/snipe
|
# bash install.sh # interactive (auto-detects Docker)
|
||||||
# bash install.sh /opt/snipe # custom install directory
|
# bash install.sh --docker # Docker Compose setup only
|
||||||
# bash install.sh ~/snipe --no-docker # force no-Docker path even if Docker present
|
# bash install.sh --bare-metal # conda or venv + uvicorn
|
||||||
|
# bash install.sh --help
|
||||||
#
|
#
|
||||||
# Requirements (Docker path): Docker with Compose plugin, Git
|
# No account or API key required. eBay credentials are optional (faster searches).
|
||||||
# Requirements (no-Docker path): Python 3.11+, Node.js 20+, Git, xvfb (system)
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
INSTALL_DIR="${1:-$HOME/snipe}"
|
# ── Terminal colours ───────────────────────────────────────────────────────────
|
||||||
FORCE_NO_DOCKER="${2:-}"
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}▶${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}✗${NC} $*" >&2; }
|
||||||
|
header() { echo; echo -e "${BOLD}$*${NC}"; printf '%0.s─' {1..60}; echo; }
|
||||||
|
dim() { echo -e "${DIM}$*${NC}"; }
|
||||||
|
ask() { echo -e "${CYAN}?${NC} ${BOLD}$*${NC}"; }
|
||||||
|
fail() { error "$*"; exit 1; }
|
||||||
|
|
||||||
|
# ── Paths ──────────────────────────────────────────────────────────────────────
|
||||||
|
SNIPE_CONFIG_DIR="${HOME}/.config/circuitforge"
|
||||||
|
SNIPE_ENV_FILE="${SNIPE_CONFIG_DIR}/snipe.env"
|
||||||
|
SNIPE_VENV_DIR="${SNIPE_CONFIG_DIR}/venv"
|
||||||
FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge"
|
FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge"
|
||||||
CONDA_ENV="cf"
|
|
||||||
|
|
||||||
info() { echo " [snipe] $*"; }
|
# Default install directory. Overridable:
|
||||||
ok() { echo "✓ $*"; }
|
# SNIPE_DIR=/opt/snipe bash install.sh
|
||||||
warn() { echo "! $*"; }
|
SNIPE_INSTALL_DIR="${SNIPE_DIR:-${HOME}/snipe}"
|
||||||
fail() { echo "✗ $*" >&2; exit 1; }
|
|
||||||
hr() { echo "────────────────────────────────────────────────────────"; }
|
|
||||||
|
|
||||||
echo ""
|
# ── Argument parsing ───────────────────────────────────────────────────────────
|
||||||
echo " Snipe — self-hosted installer"
|
MODE_FORCE=""
|
||||||
echo " Install directory: $INSTALL_DIR"
|
for arg in "$@"; do
|
||||||
echo ""
|
case "$arg" in
|
||||||
|
--bare-metal) MODE_FORCE="bare-metal" ;;
|
||||||
|
--docker) MODE_FORCE="docker" ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: bash install.sh [--docker|--bare-metal|--help]"
|
||||||
|
echo
|
||||||
|
echo " --docker Docker Compose install (recommended)"
|
||||||
|
echo " --bare-metal conda or pip venv + uvicorn"
|
||||||
|
echo " --help Show this message"
|
||||||
|
echo
|
||||||
|
echo " Set SNIPE_DIR=/path to change the install directory (default: ~/snipe)"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# ── Detect capabilities ──────────────────────────────────────────────────────
|
# ── Banner ─────────────────────────────────────────────────────────────────────
|
||||||
|
echo
|
||||||
|
echo -e "${BOLD} 🎯 Snipe — eBay listing intelligence${NC}"
|
||||||
|
echo -e "${DIM} Bid with confidence. Privacy-first, no account required.${NC}"
|
||||||
|
echo -e "${DIM} Part of the Circuit Forge LLC suite (BSL 1.1)${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ── System checks ──────────────────────────────────────────────────────────────
|
||||||
|
header "System checks"
|
||||||
|
|
||||||
HAS_DOCKER=false
|
HAS_DOCKER=false
|
||||||
HAS_CONDA=false
|
HAS_CONDA=false
|
||||||
|
HAS_CONDA_CMD=""
|
||||||
HAS_PYTHON=false
|
HAS_PYTHON=false
|
||||||
HAS_NODE=false
|
HAS_NODE=false
|
||||||
|
HAS_CHROMIUM=false
|
||||||
|
HAS_XVFB=false
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || fail "Git is required. Install: sudo apt-get install git"
|
||||||
|
ok "Git found"
|
||||||
|
|
||||||
docker compose version >/dev/null 2>&1 && HAS_DOCKER=true
|
docker compose version >/dev/null 2>&1 && HAS_DOCKER=true
|
||||||
conda --version >/dev/null 2>&1 && HAS_CONDA=true
|
if $HAS_DOCKER; then ok "Docker (Compose plugin) found"; fi
|
||||||
python3 --version >/dev/null 2>&1 && HAS_PYTHON=true
|
|
||||||
node --version >/dev/null 2>&1 && HAS_NODE=true
|
|
||||||
command -v git >/dev/null 2>&1 || fail "Git is required. Install with: sudo apt-get install git"
|
|
||||||
|
|
||||||
# Honour --no-docker flag
|
# Detect conda / mamba / micromamba in preference order
|
||||||
[[ "$FORCE_NO_DOCKER" == "--no-docker" ]] && HAS_DOCKER=false
|
for _c in conda mamba micromamba; do
|
||||||
|
if command -v "$_c" >/dev/null 2>&1; then
|
||||||
|
HAS_CONDA=true
|
||||||
|
HAS_CONDA_CMD="$_c"
|
||||||
|
ok "Conda manager found: $_c"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
if $HAS_DOCKER; then
|
# Python 3.11+ check
|
||||||
INSTALL_PATH="docker"
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
ok "Docker found — using Docker install path (recommended)"
|
_py_ok=$(python3 -c "import sys; print(sys.version_info >= (3,11))" 2>/dev/null || echo "False")
|
||||||
|
if [[ "$_py_ok" == "True" ]]; then
|
||||||
|
HAS_PYTHON=true
|
||||||
|
ok "Python 3.11+ found ($(python3 --version))"
|
||||||
|
else
|
||||||
|
warn "Python found but version is below 3.11 ($(python3 --version)) — bare-metal path may fail"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v node >/dev/null 2>&1 && HAS_NODE=true
|
||||||
|
if $HAS_NODE; then ok "Node.js found ($(node --version))"; fi
|
||||||
|
|
||||||
|
# Chromium / Google Chrome — needed for the Kasada-bypass scraper
|
||||||
|
for _chrome in google-chrome chromium-browser chromium; do
|
||||||
|
if command -v "$_chrome" >/dev/null 2>&1; then
|
||||||
|
HAS_CHROMIUM=true
|
||||||
|
ok "Chromium/Chrome found: $_chrome"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if ! $HAS_CHROMIUM; then
|
||||||
|
warn "Chromium / Google Chrome not found."
|
||||||
|
warn "Snipe uses headed Chromium + Xvfb to bypass eBay's Kasada anti-bot."
|
||||||
|
warn "The installer will install Chromium via Playwright. If that fails,"
|
||||||
|
warn "add eBay API credentials to .env to use the API adapter instead."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Xvfb — virtual framebuffer for headed Chromium on headless servers
|
||||||
|
command -v Xvfb >/dev/null 2>&1 && HAS_XVFB=true
|
||||||
|
if $HAS_XVFB; then ok "Xvfb found"; fi
|
||||||
|
|
||||||
|
# ── Mode selection ─────────────────────────────────────────────────────────────
|
||||||
|
header "Install mode"
|
||||||
|
|
||||||
|
INSTALL_MODE=""
|
||||||
|
if [[ -n "$MODE_FORCE" ]]; then
|
||||||
|
INSTALL_MODE="$MODE_FORCE"
|
||||||
|
info "Mode forced: $INSTALL_MODE"
|
||||||
|
elif $HAS_DOCKER; then
|
||||||
|
INSTALL_MODE="docker"
|
||||||
|
ok "Docker available — using Docker install (recommended)"
|
||||||
|
dim " Pass --bare-metal to override"
|
||||||
elif $HAS_PYTHON; then
|
elif $HAS_PYTHON; then
|
||||||
INSTALL_PATH="python"
|
INSTALL_MODE="bare-metal"
|
||||||
warn "Docker not found — using no-Docker path (conda or venv)"
|
warn "Docker not found — using bare-metal install"
|
||||||
else
|
else
|
||||||
fail "Docker or Python 3.11+ is required. Install Docker: https://docs.docker.com/get-docker/"
|
fail "Docker or Python 3.11+ is required. Install Docker: https://docs.docker.com/get-docker/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Clone repos ──────────────────────────────────────────────────────────────
|
# ── Clone repos ───────────────────────────────────────────────────────────────
|
||||||
|
header "Clone repositories"
|
||||||
|
|
||||||
# compose.yml and the Dockerfile both use context: .. (parent directory), so
|
# compose.yml and the Dockerfile both use context: .. (parent directory), so
|
||||||
# snipe/ and circuitforge-core/ must be siblings inside INSTALL_DIR.
|
# snipe/ and circuitforge-core/ must be siblings inside SNIPE_INSTALL_DIR.
|
||||||
SNIPE_DIR="$INSTALL_DIR/snipe"
|
REPO_DIR="$SNIPE_INSTALL_DIR"
|
||||||
CORE_DIR="$INSTALL_DIR/circuitforge-core"
|
SNIPE_DIR_ACTUAL="$REPO_DIR/snipe"
|
||||||
|
CORE_DIR="$REPO_DIR/circuitforge-core"
|
||||||
|
|
||||||
if [[ -d "$SNIPE_DIR" ]]; then
|
_clone_or_pull() {
|
||||||
info "Snipe already cloned — pulling latest..."
|
local label="$1" url="$2" dest="$3"
|
||||||
git -C "$SNIPE_DIR" pull --ff-only
|
if [[ -d "$dest/.git" ]]; then
|
||||||
|
info "$label already cloned — pulling latest..."
|
||||||
|
git -C "$dest" pull --ff-only
|
||||||
else
|
else
|
||||||
info "Cloning Snipe..."
|
info "Cloning $label..."
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$(dirname "$dest")"
|
||||||
git clone "$FORGEJO/snipe.git" "$SNIPE_DIR"
|
git clone "$url" "$dest"
|
||||||
fi
|
fi
|
||||||
ok "Snipe → $SNIPE_DIR"
|
ok "$label → $dest"
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -d "$CORE_DIR" ]]; then
|
_clone_or_pull "snipe" "$FORGEJO/snipe.git" "$SNIPE_DIR_ACTUAL"
|
||||||
info "circuitforge-core already cloned — pulling latest..."
|
_clone_or_pull "circuitforge-core" "$FORGEJO/circuitforge-core.git" "$CORE_DIR"
|
||||||
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 → $CORE_DIR"
|
|
||||||
|
|
||||||
# ── Configure environment ────────────────────────────────────────────────────
|
# ── Config file ────────────────────────────────────────────────────────────────
|
||||||
|
header "Configuration"
|
||||||
|
|
||||||
ENV_FILE="$SNIPE_DIR/.env"
|
ENV_FILE="$SNIPE_DIR_ACTUAL/.env"
|
||||||
if [[ ! -f "$ENV_FILE" ]]; then
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
cp "$SNIPE_DIR/.env.example" "$ENV_FILE"
|
cp "$SNIPE_DIR_ACTUAL/.env.example" "$ENV_FILE"
|
||||||
# Safe defaults for local installs — no eBay registration, no Heimdall
|
# Disable webhook signature verification for local installs
|
||||||
|
# (no production eBay key yet — the endpoint won't be registered)
|
||||||
sed -i 's/^EBAY_WEBHOOK_VERIFY_SIGNATURES=true/EBAY_WEBHOOK_VERIFY_SIGNATURES=false/' "$ENV_FILE"
|
sed -i 's/^EBAY_WEBHOOK_VERIFY_SIGNATURES=true/EBAY_WEBHOOK_VERIFY_SIGNATURES=false/' "$ENV_FILE"
|
||||||
ok ".env created from .env.example"
|
ok ".env created from .env.example"
|
||||||
echo ""
|
echo
|
||||||
info "Snipe works out of the box with no API keys."
|
dim " Snipe works out of the box with no API keys (scraper mode)."
|
||||||
info "Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)."
|
dim " Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)."
|
||||||
echo ""
|
dim " Edit: $ENV_FILE"
|
||||||
|
echo
|
||||||
else
|
else
|
||||||
info ".env already exists — skipping (delete it to reset)"
|
info ".env already exists — skipping (delete to reset defaults)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$SNIPE_DIR"
|
# ── License key (optional) ─────────────────────────────────────────────────────
|
||||||
|
header "CircuitForge license key (optional)"
|
||||||
|
dim " Snipe is free to self-host. A Paid/Premium key unlocks cloud features"
|
||||||
|
dim " (photo analysis, eBay OAuth). Skip this if you don't have one."
|
||||||
|
echo
|
||||||
|
ask "Enter your license key, or press Enter to skip:"
|
||||||
|
read -r _license_key || true
|
||||||
|
|
||||||
# ── Docker install path ───────────────────────────────────────────────────────
|
if [[ -n "${_license_key:-}" ]]; then
|
||||||
|
_key_re='^CFG-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
|
||||||
|
if echo "$_license_key" | grep -qP "$_key_re" 2>/dev/null || \
|
||||||
|
echo "$_license_key" | grep -qE "$_key_re" 2>/dev/null; then
|
||||||
|
# Append / uncomment Heimdall vars in .env
|
||||||
|
if grep -q "^# HEIMDALL_URL=" "$ENV_FILE" 2>/dev/null; then
|
||||||
|
sed -i "s|^# HEIMDALL_URL=.*|HEIMDALL_URL=https://license.circuitforge.tech|" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
echo "HEIMDALL_URL=https://license.circuitforge.tech" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
# Write or replace CF_LICENSE_KEY
|
||||||
|
if grep -q "^CF_LICENSE_KEY=" "$ENV_FILE" 2>/dev/null; then
|
||||||
|
sed -i "s|^CF_LICENSE_KEY=.*|CF_LICENSE_KEY=${_license_key}|" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
echo "CF_LICENSE_KEY=${_license_key}" >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
ok "License key saved to .env"
|
||||||
|
else
|
||||||
|
warn "Key format not recognised (expected CFG-XXXX-XXXX-XXXX-XXXX) — skipping."
|
||||||
|
warn "Edit $ENV_FILE to add it manually."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "No license key entered — self-hosted free tier."
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$INSTALL_PATH" == "docker" ]]; then
|
# ── Docker install ─────────────────────────────────────────────────────────────
|
||||||
|
_install_docker() {
|
||||||
|
header "Docker install"
|
||||||
|
|
||||||
|
cd "$SNIPE_DIR_ACTUAL"
|
||||||
info "Building Docker images (~1 GB download on first run)..."
|
info "Building Docker images (~1 GB download on first run)..."
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|
||||||
info "Starting Snipe..."
|
info "Starting Snipe..."
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
echo ""
|
echo
|
||||||
ok "Snipe is running!"
|
ok "Snipe is running!"
|
||||||
hr
|
printf '%0.s─' {1..60}; echo
|
||||||
echo " Web UI: http://localhost:8509"
|
echo -e " ${GREEN}Web UI:${NC} http://localhost:8509"
|
||||||
echo " API: http://localhost:8510/docs"
|
echo -e " ${GREEN}API:${NC} http://localhost:8510/docs"
|
||||||
echo ""
|
echo
|
||||||
echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}"
|
echo -e " ${DIM}Manage: cd $SNIPE_DIR_ACTUAL && ./manage.sh {start|stop|restart|logs|test}${NC}"
|
||||||
hr
|
printf '%0.s─' {1..60}; echo
|
||||||
echo ""
|
echo
|
||||||
exit 0
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
# ── No-Docker install path ───────────────────────────────────────────────────
|
# ── Bare-metal install ─────────────────────────────────────────────────────────
|
||||||
|
_install_xvfb() {
|
||||||
# System deps: Xvfb is required for Playwright (Kasada bypass via headed Chromium)
|
if $HAS_XVFB; then return; fi
|
||||||
if ! command -v Xvfb >/dev/null 2>&1; then
|
|
||||||
info "Installing Xvfb (required for eBay scraper)..."
|
info "Installing Xvfb (required for eBay scraper)..."
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
sudo apt-get install -y --no-install-recommends xvfb
|
sudo apt-get install -y --no-install-recommends xvfb
|
||||||
|
ok "Xvfb installed"
|
||||||
elif command -v dnf >/dev/null 2>&1; then
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
sudo dnf install -y xorg-x11-server-Xvfb
|
sudo dnf install -y xorg-x11-server-Xvfb
|
||||||
|
ok "Xvfb installed"
|
||||||
elif command -v brew >/dev/null 2>&1; then
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
warn "macOS: Xvfb not available. The scraper fallback may fail."
|
warn "macOS: Xvfb not available via Homebrew."
|
||||||
|
warn "The scraper (Kasada bypass) will not work on macOS."
|
||||||
warn "Add eBay API credentials to .env to use the API adapter instead."
|
warn "Add eBay API credentials to .env to use the API adapter instead."
|
||||||
else
|
else
|
||||||
warn "Could not install Xvfb automatically. Install it with your package manager."
|
warn "Could not install Xvfb automatically. Install it with your system package manager."
|
||||||
fi
|
warn " Debian/Ubuntu: sudo apt-get install xvfb"
|
||||||
|
warn " Fedora/RHEL: sudo dnf install xorg-x11-server-Xvfb"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Python environment setup ─────────────────────────────────────────────────
|
_setup_python_env() {
|
||||||
|
|
||||||
if $HAS_CONDA; then
|
if $HAS_CONDA; then
|
||||||
info "Setting up conda environment '$CONDA_ENV'..."
|
info "Setting up conda environment (manager: $HAS_CONDA_CMD)..."
|
||||||
if conda env list | grep -q "^$CONDA_ENV "; then
|
_env_name="cf"
|
||||||
info "Conda env '$CONDA_ENV' already exists — updating..."
|
if "$HAS_CONDA_CMD" env list 2>/dev/null | grep -q "^${_env_name} "; then
|
||||||
conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR"
|
info "Conda env '$_env_name' already exists — updating packages..."
|
||||||
conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR"
|
|
||||||
else
|
else
|
||||||
conda create -n "$CONDA_ENV" python=3.11 -y
|
"$HAS_CONDA_CMD" create -n "$_env_name" python=3.11 -y
|
||||||
conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR"
|
|
||||||
conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR"
|
|
||||||
fi
|
fi
|
||||||
conda run -n "$CONDA_ENV" playwright install chromium
|
"$HAS_CONDA_CMD" run -n "$_env_name" pip install --quiet -e "$CORE_DIR"
|
||||||
conda run -n "$CONDA_ENV" playwright install-deps chromium
|
"$HAS_CONDA_CMD" run -n "$_env_name" pip install --quiet -e "$SNIPE_DIR_ACTUAL"
|
||||||
PYTHON_RUN="conda run -n $CONDA_ENV"
|
"$HAS_CONDA_CMD" run -n "$_env_name" playwright install chromium
|
||||||
ok "Conda environment '$CONDA_ENV' ready"
|
"$HAS_CONDA_CMD" run -n "$_env_name" playwright install-deps chromium
|
||||||
|
PYTHON_BIN="$HAS_CONDA_CMD run -n $_env_name"
|
||||||
|
ok "Conda environment '$_env_name' ready"
|
||||||
else
|
else
|
||||||
info "Setting up Python venv at $SNIPE_DIR/.venv ..."
|
info "Setting up pip venv at $SNIPE_VENV_DIR ..."
|
||||||
python3 -m venv "$SNIPE_DIR/.venv"
|
mkdir -p "$SNIPE_CONFIG_DIR"
|
||||||
"$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$CORE_DIR"
|
python3 -m venv "$SNIPE_VENV_DIR"
|
||||||
"$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$SNIPE_DIR"
|
"$SNIPE_VENV_DIR/bin/pip" install --quiet -e "$CORE_DIR"
|
||||||
"$SNIPE_DIR/.venv/bin/playwright" install chromium
|
"$SNIPE_VENV_DIR/bin/pip" install --quiet -e "$SNIPE_DIR_ACTUAL"
|
||||||
"$SNIPE_DIR/.venv/bin/playwright" install-deps chromium
|
"$SNIPE_VENV_DIR/bin/playwright" install chromium
|
||||||
PYTHON_RUN="$SNIPE_DIR/.venv/bin"
|
"$SNIPE_VENV_DIR/bin/playwright" install-deps chromium
|
||||||
ok "Python venv ready at $SNIPE_DIR/.venv"
|
PYTHON_BIN="$SNIPE_VENV_DIR/bin"
|
||||||
|
ok "Python venv ready at $SNIPE_VENV_DIR"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Frontend ─────────────────────────────────────────────────────────────────
|
_build_frontend() {
|
||||||
|
if ! $HAS_NODE; then
|
||||||
if $HAS_NODE; then
|
warn "Node.js not found — skipping frontend build."
|
||||||
|
warn "Install Node.js 20+ from https://nodejs.org and re-run install.sh."
|
||||||
|
warn "Until then, access the API at http://localhost:8510/docs"
|
||||||
|
return
|
||||||
|
fi
|
||||||
info "Building Vue frontend..."
|
info "Building Vue frontend..."
|
||||||
cd "$SNIPE_DIR/web"
|
cd "$SNIPE_DIR_ACTUAL/web"
|
||||||
npm ci --prefer-offline --silent
|
npm ci --prefer-offline --silent
|
||||||
npm run build
|
npm run build
|
||||||
cd "$SNIPE_DIR"
|
cd "$SNIPE_DIR_ACTUAL"
|
||||||
ok "Frontend built → web/dist/"
|
ok "Frontend built → web/dist/"
|
||||||
else
|
}
|
||||||
warn "Node.js not found — skipping frontend build."
|
|
||||||
warn "Install Node.js 20+ from https://nodejs.org and re-run install.sh to build the UI."
|
|
||||||
warn "Until then, you can access the API directly at http://localhost:8510/docs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Write start/stop scripts ─────────────────────────────────────────────────
|
_write_start_scripts() {
|
||||||
|
# start-local.sh — launches the FastAPI server
|
||||||
cat > "$SNIPE_DIR/start-local.sh" << 'STARTSCRIPT'
|
cat > "$SNIPE_DIR_ACTUAL/start-local.sh" << 'STARTSCRIPT'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Start Snipe without Docker (API only — run from the snipe/ directory)
|
# Start Snipe API (bare-metal / no-Docker mode)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
if [[ -f .venv/bin/uvicorn ]]; then
|
if [[ -f "$HOME/.config/circuitforge/venv/bin/uvicorn" ]]; then
|
||||||
UVICORN=".venv/bin/uvicorn"
|
UVICORN="$HOME/.config/circuitforge/venv/bin/uvicorn"
|
||||||
elif command -v conda >/dev/null 2>&1 && conda env list | grep -q "^cf "; then
|
elif command -v conda >/dev/null 2>&1 && conda env list 2>/dev/null | grep -q "^cf "; then
|
||||||
UVICORN="conda run -n cf uvicorn"
|
UVICORN="conda run -n cf uvicorn"
|
||||||
|
elif command -v mamba >/dev/null 2>&1 && mamba env list 2>/dev/null | grep -q "^cf "; then
|
||||||
|
UVICORN="mamba run -n cf uvicorn"
|
||||||
else
|
else
|
||||||
echo "No Python env found. Run install.sh first." >&2; exit 1
|
echo "No Snipe Python environment found. Run install.sh first." >&2; exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p data
|
mkdir -p data
|
||||||
echo "Starting Snipe API on http://localhost:8510 ..."
|
echo "Starting Snipe API → http://localhost:8510 ..."
|
||||||
$UVICORN api.main:app --host 0.0.0.0 --port 8510 "${@}"
|
exec $UVICORN api.main:app --host 0.0.0.0 --port 8510 "${@}"
|
||||||
STARTSCRIPT
|
STARTSCRIPT
|
||||||
chmod +x "$SNIPE_DIR/start-local.sh"
|
chmod +x "$SNIPE_DIR_ACTUAL/start-local.sh"
|
||||||
|
|
||||||
# Frontend serving (if built)
|
# serve-ui.sh — serves the built Vue frontend (dev only)
|
||||||
cat > "$SNIPE_DIR/serve-ui.sh" << 'UISCRIPT'
|
cat > "$SNIPE_DIR_ACTUAL/serve-ui.sh" << 'UISCRIPT'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Serve the pre-built Vue frontend on port 8509 (dev only — use nginx for production)
|
# Serve the pre-built Vue frontend (dev only — use nginx for production).
|
||||||
|
# See docs/nginx-self-hosted.conf for a production nginx config.
|
||||||
cd "$(dirname "$0")/web/dist"
|
cd "$(dirname "$0")/web/dist"
|
||||||
python3 -m http.server 8509
|
echo "Serving Snipe UI → http://localhost:8509 (Ctrl+C to stop)"
|
||||||
|
exec python3 -m http.server 8509
|
||||||
UISCRIPT
|
UISCRIPT
|
||||||
chmod +x "$SNIPE_DIR/serve-ui.sh"
|
chmod +x "$SNIPE_DIR_ACTUAL/serve-ui.sh"
|
||||||
|
|
||||||
echo ""
|
ok "Start scripts written"
|
||||||
ok "Snipe installed (no-Docker mode)"
|
}
|
||||||
hr
|
|
||||||
echo " Start API: cd $SNIPE_DIR && ./start-local.sh"
|
_install_bare_metal() {
|
||||||
echo " Serve UI: cd $SNIPE_DIR && ./serve-ui.sh (separate terminal)"
|
header "Bare-metal install"
|
||||||
echo " API docs: http://localhost:8510/docs"
|
_install_xvfb
|
||||||
echo " Web UI: http://localhost:8509 (after ./serve-ui.sh)"
|
_setup_python_env
|
||||||
echo ""
|
_build_frontend
|
||||||
echo " For production, point nginx at web/dist/ and proxy /api/ to localhost:8510"
|
_write_start_scripts
|
||||||
hr
|
|
||||||
echo ""
|
echo
|
||||||
|
ok "Snipe installed (bare-metal mode)"
|
||||||
|
printf '%0.s─' {1..60}; echo
|
||||||
|
echo -e " ${GREEN}Start API:${NC} cd $SNIPE_DIR_ACTUAL && ./start-local.sh"
|
||||||
|
echo -e " ${GREEN}Serve UI:${NC} cd $SNIPE_DIR_ACTUAL && ./serve-ui.sh ${DIM}(separate terminal)${NC}"
|
||||||
|
echo -e " ${GREEN}API docs:${NC} http://localhost:8510/docs"
|
||||||
|
echo -e " ${GREEN}Web UI:${NC} http://localhost:8509 ${DIM}(after ./serve-ui.sh)${NC}"
|
||||||
|
echo
|
||||||
|
echo -e " ${DIM}For production, configure nginx to proxy /api/ to localhost:8510${NC}"
|
||||||
|
echo -e " ${DIM}and serve web/dist/ as the document root.${NC}"
|
||||||
|
echo -e " ${DIM}See: $SNIPE_DIR_ACTUAL/docs/nginx-self-hosted.conf${NC}"
|
||||||
|
printf '%0.s─' {1..60}; echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ───────────────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
if [[ "$INSTALL_MODE" == "docker" ]]; then
|
||||||
|
_install_docker
|
||||||
|
else
|
||||||
|
_install_bare_metal
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
|
|
||||||
63
mkdocs.yml
Normal file
63
mkdocs.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
site_name: Snipe
|
||||||
|
site_description: eBay trust scoring before you bid — catch scammers, flag suspicious prices, surface duplicate photos.
|
||||||
|
site_author: Circuit Forge LLC
|
||||||
|
site_url: https://docs.circuitforge.tech/snipe
|
||||||
|
repo_url: https://git.opensourcesolarpunk.com/Circuit-Forge/snipe
|
||||||
|
repo_name: Circuit-Forge/snipe
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
palette:
|
||||||
|
- scheme: default
|
||||||
|
primary: deep orange
|
||||||
|
accent: orange
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: Switch to dark mode
|
||||||
|
- scheme: slate
|
||||||
|
primary: deep orange
|
||||||
|
accent: orange
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: Switch to light mode
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.expand
|
||||||
|
- navigation.top
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
- content.code.copy
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- tables
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Home: index.md
|
||||||
|
- Getting Started:
|
||||||
|
- Installation: getting-started/installation.md
|
||||||
|
- Quick Start: getting-started/quick-start.md
|
||||||
|
- eBay API Keys (Optional): getting-started/ebay-api.md
|
||||||
|
- User Guide:
|
||||||
|
- Searching: user-guide/searching.md
|
||||||
|
- Trust Scores: user-guide/trust-scores.md
|
||||||
|
- Red Flags: user-guide/red-flags.md
|
||||||
|
- Community Blocklist: user-guide/blocklist.md
|
||||||
|
- Settings: user-guide/settings.md
|
||||||
|
- Reference:
|
||||||
|
- Trust Score Algorithm: reference/trust-scoring.md
|
||||||
|
- Tier System: reference/tier-system.md
|
||||||
|
- Architecture: reference/architecture.md
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "snipe"
|
name = "snipe"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
description = "Auction listing monitor and trust scorer"
|
description = "Auction listing monitor and trust scorer"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue