# 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` + `:` 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.