ead2cdbc3c
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
239 lines
9.8 KiB
Markdown
239 lines
9.8 KiB
Markdown
# OverSnitch
|
||
|
||
A self-hosted dashboard for monitoring Overseerr/Jellyseerr users — who's requesting, how much storage they're consuming, how often they actually watch what they request, and whether anything needs your attention.
|
||
|
||
Built with Next.js 16, TypeScript, and Tailwind CSS.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- **Leaderboard** — per-user request count, total storage, average GB per request, and optional Tautulli watch stats (plays, watch hours), each ranked against the full userbase
|
||
- **User detail pages** — click any user in the leaderboard to see their full profile: stat cards, an activity chart with 1W/1M/3M/1Y timeframes, and a complete request history sorted newest-first
|
||
- **Activity chart** — single metric picker (Requests · Storage · Watch Hours · Storage Load) × timeframe (1W/1M/3M/1Y), with both the user's own average and the server-wide average shown as reference lines. Storage Load is a running cumulative GB ÷ watch hours — a new request spikes the line up and subsequent watching drags it back down.
|
||
- **Alerting** — automatic alerts for stalled downloads, neglected requesters, and abusive patterns, with open/close state, notes, and auto-resolve when conditions clear
|
||
- **Discord notifications** — posts a structured embed to a webhook whenever a new alert opens or a resolved one returns
|
||
- **Settings UI** — configure all service URLs and API keys from the dashboard; no need to touch `.env.local` after initial setup
|
||
- **SWR caching** — stats are cached server-side for 5 minutes and seeded from localStorage on the client, so the dashboard is instant on return visits
|
||
|
||
---
|
||
|
||
## Setup
|
||
|
||
### Option A — Docker Compose (recommended)
|
||
|
||
Images are published to `gitea.thewrightserver.net/josh/oversnitch` on every push to `main` by the Gitea Actions workflow at [.gitea/workflows/build.yml](.gitea/workflows/build.yml).
|
||
|
||
On the deployment host:
|
||
|
||
```bash
|
||
# Pull the latest image and start
|
||
docker login gitea.thewrightserver.net # if the registry is private
|
||
docker compose pull
|
||
docker compose up -d
|
||
```
|
||
|
||
The bundled [docker-compose.yml](docker-compose.yml) mounts `./data` into `/app/data`, so `alerts.db` and `settings.json` persist across restarts. The dashboard listens on `http://localhost:3000` — after first boot, click the gear icon and enter your service URLs and API keys there.
|
||
|
||
To configure services via environment instead of the Settings UI, uncomment the relevant lines in `docker-compose.yml` (see the full list in *Option B*).
|
||
|
||
### Option B — Local Node
|
||
|
||
```bash
|
||
git clone https://gitea.thewrightserver.net/josh/OverSnitch.git
|
||
cd OverSnitch
|
||
npm install
|
||
npm run dev # or: npm run build && npm start
|
||
```
|
||
|
||
Configure through the Settings UI, or create `.env.local` with any of:
|
||
|
||
```env
|
||
# Required
|
||
SEERR_URL=http://overseerr:5055
|
||
SEERR_API=your_overseerr_api_key
|
||
|
||
RADARR_URL=http://radarr:7878
|
||
RADARR_API=your_radarr_api_key
|
||
|
||
SONARR_URL=http://sonarr:8989
|
||
SONARR_API=your_sonarr_api_key
|
||
|
||
# Optional — enables watch time stats and ghost/watch-rate alerts
|
||
TAUTULLI_URL=http://tautulli:8181
|
||
TAUTULLI_API=your_tautulli_api_key
|
||
|
||
# Optional — Discord webhook for new-alert notifications
|
||
DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
|
||
|
||
# Optional — if your services use self-signed certs
|
||
# NODE_TLS_REJECT_UNAUTHORIZED=0
|
||
```
|
||
|
||
Values in `data/settings.json` (written by the Settings UI) take precedence over env vars.
|
||
|
||
### CI / Registry
|
||
|
||
The Gitea workflow expects two repository-level config items:
|
||
|
||
| Kind | Name | Value |
|
||
|---|---|---|
|
||
| Variable | `REGISTRY_URL` | e.g. `gitea.thewrightserver.net` |
|
||
| Secret | `REGISTRY_TOKEN` | a Gitea access token with `write:package` scope |
|
||
|
||
On every push to `main`, the workflow builds a multi-stage Alpine image and pushes `:latest` + `:<sha7>` tags. Pushing a `v*` tag additionally publishes that tag.
|
||
|
||
---
|
||
|
||
## Discord Notifications
|
||
|
||
When configured, OverSnitch posts a structured embed to your Discord channel whenever an alert is newly opened or reopens after being resolved. Already-open alerts refreshing their data do not re-notify.
|
||
|
||
Each embed is formatted by category:
|
||
|
||
| Category | Fields shown |
|
||
|---|---|
|
||
| Not Downloaded | Requested by · Approved N ago · Status |
|
||
| Incomplete Download | Requested by · Approved N ago · Downloaded X/Y episodes |
|
||
| Pending Approval | Requested by · Waiting N days |
|
||
| Ghost Requester | User · description |
|
||
| Low Watch Rate | User · Watch rate · Plays · Requests |
|
||
|
||
Configure the webhook URL in the Settings UI or via `DISCORD_WEBHOOK` in `.env.local`. Use the **Test** button to send a sample embed before saving.
|
||
|
||
---
|
||
|
||
## Alerts
|
||
|
||
Alerts are generated on every stats refresh and persisted in `data/alerts.db` (SQLite, gitignored). They have two states — **Open** and **Closed** — and can be manually closed with a per-category cooldown, or auto-resolved when the underlying condition clears.
|
||
|
||
The alert detail page shows structured metadata (requesters, age, episode progress bars, watch-rate stats), direct links to the item in Radarr/Sonarr, a link to the Overseerr/Jellyseerr media page, and a comment thread for notes.
|
||
|
||
### Content alerts
|
||
|
||
These are keyed per piece of media, not per user. If multiple users requested the same item they're grouped into a single alert.
|
||
|
||
---
|
||
|
||
#### Not Downloaded
|
||
|
||
> A movie or TV show was approved but no file exists in Radarr/Sonarr.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `UNFULFILLED_MIN_AGE_HOURS` | `12` | Hours since approval before alerting. Prevents noise on brand-new requests. |
|
||
|
||
- Skipped if Radarr reports `isAvailable: false` (unreleased) or Sonarr reports `status: "upcoming"`.
|
||
- **Auto-resolves** when the file appears.
|
||
- Manual close: no cooldown — reopens on the next refresh if the file still isn't there.
|
||
|
||
---
|
||
|
||
#### Incomplete Download
|
||
|
||
> An ended TV series is missing one or more episodes.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `UNFULFILLED_MIN_AGE_HOURS` | `12` | Hours since approval before alerting. |
|
||
| Completion threshold | `100%` | Any missing episode on a finished series triggers this alert. |
|
||
|
||
- Only fires for series with `status: "ended"` in Sonarr. Continuing shows are excluded because missing episodes may not have aired yet.
|
||
- Completion is calculated as `episodeFileCount / totalEpisodeCount` (not Sonarr's `percentOfEpisodes`, which measures against monitored episodes only).
|
||
- **Auto-resolves** when all episodes are on disk.
|
||
- Manual close: no cooldown — reopens on the next refresh if episodes are still missing.
|
||
|
||
---
|
||
|
||
#### Pending Approval
|
||
|
||
> A request has been sitting unapproved for too long.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `PENDING_MIN_AGE_DAYS` | `2` | Days a request must be pending before alerting. |
|
||
|
||
- One alert per request item, not per user.
|
||
- Skipped if the content is unreleased.
|
||
- **Auto-resolves** when the request is approved or declined.
|
||
- Manual close: no cooldown — reopens on the next refresh if still pending.
|
||
|
||
---
|
||
|
||
### User behavior alerts
|
||
|
||
These fire once per user. Ghost Requester takes priority over Low Watch Rate — a user will only ever have one behavior alert open at a time. Both require the user to be "established" (at least one request older than `USER_MIN_AGE_DAYS`) to avoid flagging new users.
|
||
|
||
> Requires Tautulli to be configured.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `USER_MIN_AGE_DAYS` | `14` | Days since oldest request before a user is eligible for behavior alerts. |
|
||
|
||
---
|
||
|
||
#### Ghost Requester
|
||
|
||
> A user hasn't watched anything on Plex since before their last N approved requests were made.
|
||
|
||
Rather than checking lifetime play counts, this looks at recency: if a user's last Plex activity predates all of their most recent N approved requests, they're not watching what they're requesting.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `GHOST_RECENT_REQUESTS` | `5` | Number of recent approved requests to evaluate. Also the minimum required before the alert can fire. |
|
||
|
||
- Manual close cooldown: **7 days**.
|
||
|
||
---
|
||
|
||
#### Low Watch Rate
|
||
|
||
> A user watches a small fraction of what they request.
|
||
|
||
| Parameter | Default | Description |
|
||
|---|---|---|
|
||
| `MIN_REQUESTS_WATCHRATE` | `10` | Minimum requests before the ratio is considered meaningful. |
|
||
| `LOW_WATCH_RATE` | `0.2` | Ratio of `plays / requests` below which an alert fires (default: under 20%). |
|
||
|
||
- Manual close cooldown: **7 days**.
|
||
|
||
---
|
||
|
||
### System alerts
|
||
|
||
#### No Tautulli Watch Data
|
||
|
||
> Tautulli is configured but no plays matched any Overseerr user.
|
||
|
||
This usually means emails don't align between the two services. Check that users have the same email address in both Overseerr and Tautulli (or that display names match as a fallback).
|
||
|
||
- Manual close: no cooldown.
|
||
|
||
---
|
||
|
||
## Alert lifecycle
|
||
|
||
```
|
||
Condition detected
|
||
│
|
||
▼
|
||
[OPEN] ◄──────────────────────────────────────┐
|
||
│ │
|
||
┌────┴───────────────┐ Cooldown │
|
||
│ │ expires │
|
||
▼ ▼ │
|
||
Condition Manually │
|
||
clears closed │
|
||
│ │ │
|
||
▼ ▼ │
|
||
[AUTO-RESOLVED] [CLOSED] ──── Condition ────────┘
|
||
no cooldown cooldown returns after
|
||
reopens suppresses cooldown
|
||
immediately re-open
|
||
```
|
||
|
||
- **Auto-resolved** alerts reopen immediately if the condition returns.
|
||
- **Content alerts** (unfulfilled, pending) have no cooldown on manual close — they reopen on the next refresh if the condition still exists. Closing is an acknowledgment, not a suppression.
|
||
- **User-behavior alerts** (ghost, watchrate) suppress re-opening for 7 days after a manual close.
|
||
- Reopening a manually closed alert via the UI always clears the cooldown immediately.
|