> ## Documentation Index
> Fetch the complete documentation index at: https://docs.augustin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# riverbot

riverbot polls [recreation.gov](https://www.recreation.gov) for whitewater permit availability and pushes an [ntfy](/ntfy) alert when a spot opens inside a target date window — a cancellation/quota-release watcher. It's a sleep-loop job with no web UI, same shape as [Janitorr](/janitorr).

## Data source

An unauthenticated monthly endpoint returns per-division, per-day counts:

```
GET https://www.recreation.gov/api/permits/{permit}/availability/month?start_date=YYYY-MM-01T00:00:00.000Z
```

The signal is `payload.availability[division].date_availability[date].remaining > 0`. The endpoint is monthly, so a window spans one call per month. Division ids come from `GET /api/permitcontent/{permit}` — never guessed.

## Targets

Configured in `targets.yaml`. Division ids are resolved and pinned:

| River                              | Permit | Division | Window          |
| ---------------------------------- | ------ | -------- | --------------- |
| Middle Fork Salmon                 | 234623 | 377      | Jun 1 – Jul 20  |
| Main Salmon                        | 234622 | 376      | Jun 20 – Sep 20 |
| Selway                             | 234624 | 378      | Jun 1 – Jul 15  |
| Dinosaur – Green (Gates of Lodore) | 250014 | 380      | May 15 – Sep 1  |
| Dinosaur – Yampa (Deerlodge)       | 250014 | 371      | May 15 – Jul 15 |

Dinosaur permit `250014` also exposes a `1250014` "Combined Yampa/Green" division — a combined-trip artifact, not one of the two launch segments, so it's excluded.

## Behavior

* **Edge-triggered.** State is keyed `permit:division:date` in `./data/state.json`. A date alerts once when it goes open and re-arms when it closes, so a later reopen alerts again.
* **Close-debounce.** A reopened date only re-alerts after it has read closed *continuously* for `reopen_debounce_minutes` (default 20). This absorbs transient flicker — a scarce date briefly zeroes while two spots sit in someone's checkout cart — without muting a genuine book-then-cancel. New openings are never delayed; only re-alerts are gated. Transitions are logged (`CLOSE`/`FLAP`/`ARMED`) so real open/closed durations can be measured.
* **Grouped notifications.** Newly-open dates are merged into calendar-consecutive runs — one bookable stretch is one notification (title `Main Salmon: Sep 8-20`), not one ping per day. A gap splits the run; a run that crosses a month boundary reads `Aug 30 - Sep 2`. Titles carry the river and date span only — no emoji, no remaining-spot count.
* **Cold start alerts.** On first run (no state file) every currently-open date inside a window alerts once — the bot proves it's working by surfacing what's open now, not just future changes. After that, state suppresses re-alerts until a date closes and reopens.
* **Polling.** \~120s interval with ±25% jitter. Permit months are fetched once and shared across that permit's divisions; calls are spaced a few seconds apart to stay polite to recreation.gov's Akamai edge. A real browser User-Agent is required.
* **Fault tolerance.** A cycle never crashes the loop. A `403`/`429` block self-alerts (hourly cooldown) — silence there would look identical to "nothing available."

## Operations

* **Image:** built locally, pushed to `192.168.1.100:5000/riverbot:latest`. Rebuild with `bash scripts/rebuild-images.sh riverbot`.
* **Logs:** `docker service logs riverbot_riverbot` (or Dozzle).
* **Re-baseline:** delete `riverbot/data/state.json` and restart — the next cycle re-alerts every currently-open in-window date.
* **Add a river:** resolve its division from `permitcontent/{id}`, add an entry to `targets.yaml`, restart.
* **Swap the notifier:** `notify()` in `riverbot.py` is the only ntfy-aware seam.
