# sb-scout

Rank [Hetzner Serverbörse](https://www.hetzner.com/sb/) (Server Auction) listings by value.

Fetches the live public auction feed, caches it in an XDG-compliant cache
directory, filters servers by your criteria, scores them across CPU, RAM, and
storage value per unit of net price, and prints ranked results.

**Zero runtime dependencies** — pure Python 3.11+ stdlib.

## Install

```bash
# Nix flake
nix run github:cultscale/sb-scout

# pip
pip install .
sb-scout --help

# From source
PYTHONPATH=src python -m sb_scout --help
```

## Quick start

```bash
sb-scout                                              # defaults (USD feed)
sb-scout --currency EUR                              # switch to the EUR feed
sb-scout --profile ecc                               # ECC-only
sb-scout --profile mixed-storage                     # 2 SSD/NVMe + 2 HDD
sb-scout --profile ecc --profile mixed-storage       # compose both
sb-scout --profile ecc,gpu,budget                    # comma-separated composition
sb-scout --profile ssd-compute                       # convenience combo profile
sb-scout --json                                      # structured JSON to stdout
sb-scout --watch 300                                 # poll every 5 minutes
sb-scout --diff --history-file ~/.local/share/sb-scout/history.jsonl
```

## Configuration

Settings are resolved by layering:

```text
built-in defaults → config file [defaults] → active profiles → CLI flags
```

Pricing is treated as net / ex-VAT from Hetzner's feed.

Profiles are composable. Later profiles override earlier ones.

### Config file

```bash
sb-scout --init-config > sb-scout.toml
```

Search order:
1. `--config PATH`
2. `$SB_SCOUT_CONFIG`
3. `./sb-scout.toml`
4. `~/.config/sb-scout/config.toml`

The feed cache defaults to `~/.cache/sb-scout/feeds` (or `$XDG_CACHE_HOME/sb-scout/feeds`).

## Profiles

Profiles are **orthogonal filters/sorting presets** where possible.
Use repeated `--profile` flags or comma-separated names to compose them.

Built-in atomic profiles:
- `ecc` — ECC-only
- `mixed-storage` — at least 2 SSD/NVMe and 2 HDD
- `ssd-only` — SSD/NVMe only
- `high-memory` — at least 64 GB RAM
- `gpu` — GPU-tagged servers
- `budget` — price cap €60/month
- `storage-dense` — sort by raw storage value + minimum 8 TB

Built-in convenience profiles:
- `ssd-compute` — includes `ecc + ssd-only + high-memory`
- `cultscale` — compatibility alias for `ecc + mixed-storage`

Examples:

```bash
sb-scout --list-profiles
sb-scout --profile ecc --profile mixed-storage
sb-scout --profile ecc,gpu,budget
sb-scout --show-config --profile ssd-compute
```

You can also set default composed profiles in config:

```toml
[defaults]
active_profiles = ["ecc", "mixed-storage"]
```

## Alerts

Threshold alerts can trigger both shell commands and email via local `msmtp`.

```toml
[defaults.alerts]
min_overall_score = 0.80
max_price = 40.0
notify_command = "notify-send 'sb-scout: deal found'"
email_to = ["ops@example.com"]
email_from = "sb-scout@example.com"
email_subject = "sb-scout alert"
email_account = "default"
```

CLI equivalents:

```bash
sb-scout \
  --alert-min-overall 0.80 \
  --alert-email-to ops@example.com \
  --alert-email-from sb-scout@example.com
```

## History & diff

```bash
sb-scout --history-file ~/.local/share/sb-scout/history.jsonl
sb-scout --diff --history-file ~/.local/share/sb-scout/history.jsonl
```

Each run can append a JSONL snapshot; `--diff` compares the current run against
the last stored snapshot.

## Extending CPU models

```toml
[cpu_specs]
"AMD EPYC 9654" = { cores = 96, threads = 192 }
```

## Scoring

Three axes, each a **weighted geometric mean** of normalised value-per-EUR:

| Ranking | Components | Default weights |
| --- | --- | --- |
| **CPU rank** | threads/EUR + RAM/EUR | 0.75 / 0.25 |
| **Storage rank** | storage/EUR + RAM/EUR | 0.75 / 0.25 |
| **Overall rank** | threads/EUR + RAM/EUR + storage/EUR | 0.40 / 0.30 / 0.30 |

Bonuses: iNIC (+10%), GPU (+15%), multiplicative.

## CLI reference

### Config & profiles

| Flag | Description |
| --- | --- |
| `--config PATH` | Explicit config file |
| `--profile NAME` | Activate a profile; repeat or comma-separate to combine |
| `--cultscale` | Compatibility alias for `--profile cultscale` |
| `--currency USD\|EUR` | Select the feed currency (default: USD) |
| `--cache-dir DIR` | Override the XDG cache directory used for feed snapshots |
| `--init-config` | Print example config and exit |
| `--list-profiles` | List profiles and exit |
| `--show-config` | Show resolved config and exit |

### Filters

`--only-ecc`, `--include-non-ecc`, `--price-cap`, `--min-ram-gb`, `--max-ram-gb`,
`--min-storage`, `--max-storage`, `--min-ssd-drives`, `--min-hdd-drives`,
`--ssd-only`, `--datacenter`, `--exclude-datacenter`, `--cpu-regex`,
`--exclude-cpu-regex`, `--only-gpu`, `--price-per-thread-cap`

### Scoring

`--inic-bonus`, `--gpu-bonus`, `--cpu-rank-compute-weight`,
`--storage-rank-storage-weight`, `--overall-weights`, `--sort-by`

### Alerts

`--alert-min-overall`, `--alert-min-cpu`, `--alert-min-storage`,
`--alert-max-price` (net price), `--alert-notify`, `--alert-email-to`,
`--alert-email-from`, `--alert-email-subject`, `--alert-email-account`

### Output

| Flag | Description |
| --- | --- |
| `--top N` | Rows per section (default: 5) |
| `--format text\|markdown` | Console format |
| `--json` | Structured JSON to stdout |
| `--out-dir DIR` | Export directory |
| `--csv-out PATH` | Exact CSV path |
| `--json-out PATH` | Exact JSON path |
| `--save-markdown PATH` | Write Markdown report |
| `--no-write` | Skip file exports |
| `--history-file PATH` | JSONL price history |
| `--diff` | Diff against last snapshot |
| `--watch SECONDS` | Re-fetch loop |
| `--strict-cpu-map` | Exit on unknown CPU |

## Project structure

```text
src/sb_scout/
├── __init__.py      # Version
├── __main__.py      # python -m entry
├── cli.py           # Argument parsing, orchestration
├── config.py        # TOML config loading, profile composition
├── fetch.py         # HTTP feed fetching + XDG cache
├── models.py        # Dataclasses, constants, CPU specs
├── scoring.py       # Filtering, scoring, ranking
├── output.py        # Text, Markdown, CSV, JSON formatting
├── history.py       # Price history tracking, diff
└── alerts.py        # Threshold alerting, command hooks, msmtp email
tests/
├── conftest.py
├── test_alerts.py
├── test_cli.py
├── test_config.py
├── test_fetch.py
├── test_history.py
├── test_output.py
└── test_scoring.py
```

## Development

```bash
devenv shell
sb-scout --help
sb-scout-test      # pytest
nix build .        # builds + runs tests
```

## License

MIT
