- Settings modal (gear icon) lets you configure all service URLs and API keys from the dashboard; values persist to data/settings.json with process.env as fallback so existing .env.local setups keep working - Per-service Test button hits each service's status endpoint and reports the version on success - Discord webhook support: structured embeds per alert category (requesters, approval age, episode progress, watch-rate stats) sent on new/reopened alerts only — already-open alerts don't re-notify - Alert detail page restructured: prose descriptions replaced with labelled fields, episode progress bar for partial TV, watch-rate stat block, View in Radarr/Sonarr/Seerr action buttons, requester names link to Overseerr profiles, timestamps moved inline with status - Tab state is pure client state (no ?tab= in URL); router.back() used on alert detail for clean browser history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
220 lines
8.3 KiB
Markdown
220 lines
8.3 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
|
|
- **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
|
|
|
|
### 1. Clone and install
|
|
|
|
```bash
|
|
git clone https://gitea.thewrightserver.net/josh/OverSnitch.git
|
|
cd OverSnitch
|
|
npm install
|
|
```
|
|
|
|
### 2. Configure
|
|
|
|
**Option A — Settings UI (recommended)**
|
|
|
|
Start the app and click the gear icon in the top-right corner. Enter your service URLs and API keys, hit **Test** to verify each connection, then **Save**.
|
|
|
|
```bash
|
|
npm run dev # or: npm run build && npm start
|
|
```
|
|
|
|
Settings are written to `data/settings.json` (gitignored).
|
|
|
|
**Option B — Environment variables**
|
|
|
|
Create `.env.local` in the project root. Values here are used as fallbacks when `data/settings.json` doesn't exist or doesn't contain an override.
|
|
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
## 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.
|