Rework alert cooldown model
Content alerts (unfulfilled, pending, tautulli-no-matches) now have zero cooldown on manual close — they reopen immediately on the next refresh if the condition still exists. Closing is an acknowledgment of the current state, not a suppression of future alerts. User-behavior alerts (ghost, watchrate) keep a cooldown (7 days) so a single manual close isn't immediately undone by the next refresh. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
README.md
19
README.md
@@ -76,7 +76,7 @@ These are keyed per piece of media, not per user. If multiple users requested th
|
|||||||
|
|
||||||
- Skipped if Radarr reports `isAvailable: false` (unreleased) or Sonarr reports `status: "upcoming"`.
|
- Skipped if Radarr reports `isAvailable: false` (unreleased) or Sonarr reports `status: "upcoming"`.
|
||||||
- **Auto-resolves** when the file appears.
|
- **Auto-resolves** when the file appears.
|
||||||
- Manual close cooldown: **3 days**.
|
- Manual close: no cooldown — reopens on the next refresh if the file still isn't there.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ These are keyed per piece of media, not per user. If multiple users requested th
|
|||||||
- Only fires for series with `status: "ended"` in Sonarr. Continuing shows are excluded because missing episodes may not have aired yet.
|
- 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).
|
- Completion is calculated as `episodeFileCount / totalEpisodeCount` (not Sonarr's `percentOfEpisodes`, which measures against monitored episodes only).
|
||||||
- **Auto-resolves** when all episodes are on disk.
|
- **Auto-resolves** when all episodes are on disk.
|
||||||
- Manual close cooldown: **3 days**.
|
- Manual close: no cooldown — reopens on the next refresh if episodes are still missing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ These are keyed per piece of media, not per user. If multiple users requested th
|
|||||||
- One alert per request item, not per user.
|
- One alert per request item, not per user.
|
||||||
- Skipped if the content is unreleased.
|
- Skipped if the content is unreleased.
|
||||||
- **Auto-resolves** when the request is approved or declined.
|
- **Auto-resolves** when the request is approved or declined.
|
||||||
- Manual close cooldown: **3 days**.
|
- Manual close: no cooldown — reopens on the next refresh if still pending.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ Rather than checking lifetime play counts, this looks at recency: if a user's la
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GHOST_RECENT_REQUESTS` | `5` | Number of recent approved requests to evaluate. Also the minimum required before the alert can fire. |
|
| `GHOST_RECENT_REQUESTS` | `5` | Number of recent approved requests to evaluate. Also the minimum required before the alert can fire. |
|
||||||
|
|
||||||
- Manual close cooldown: **14 days**.
|
- Manual close cooldown: **7 days**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ Rather than checking lifetime play counts, this looks at recency: if a user's la
|
|||||||
| `MIN_REQUESTS_WATCHRATE` | `10` | Minimum requests before the ratio is considered meaningful. |
|
| `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%). |
|
| `LOW_WATCH_RATE` | `0.2` | Ratio of `plays / requests` below which an alert fires (default: under 20%). |
|
||||||
|
|
||||||
- Manual close cooldown: **14 days**.
|
- Manual close cooldown: **7 days**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ Rather than checking lifetime play counts, this looks at recency: if a user's la
|
|||||||
|
|
||||||
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).
|
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 cooldown: **1 day**.
|
- Manual close: no cooldown.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ clears closed │
|
|||||||
immediately re-open
|
immediately re-open
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Auto-resolved** alerts have no cooldown and will reopen immediately if the condition returns.
|
- **Auto-resolved** alerts reopen immediately if the condition returns.
|
||||||
- **Manually closed** alerts suppress re-opening for a category-specific number of days (see cooldowns above).
|
- **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.
|
||||||
- Reopening a manually closed alert via the UI clears the cooldown immediately.
|
- **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.
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ import {
|
|||||||
const DATA_DIR = join(process.cwd(), "data");
|
const DATA_DIR = join(process.cwd(), "data");
|
||||||
const DB_PATH = join(DATA_DIR, "alerts.json");
|
const DB_PATH = join(DATA_DIR, "alerts.json");
|
||||||
|
|
||||||
// Cooldown days applied on MANUAL close — suppresses re-opening for this long.
|
// Cooldown days applied on MANUAL close.
|
||||||
// Auto-resolved alerts have no cooldown and can reopen immediately if the
|
// 0 = no cooldown: content alerts reopen immediately on the next refresh if
|
||||||
// condition returns.
|
// the condition still exists. Closing is an acknowledgment, not a suppression.
|
||||||
|
// >0 = user-behavior alerts: suppress re-opening for this many days so a single
|
||||||
|
// acknowledgment isn't immediately undone by the next refresh.
|
||||||
const COOLDOWN: Record<string, number> = {
|
const COOLDOWN: Record<string, number> = {
|
||||||
unfulfilled: 3,
|
unfulfilled: 0,
|
||||||
pending: 3,
|
pending: 0,
|
||||||
ghost: 14,
|
ghost: 7,
|
||||||
watchrate: 14,
|
watchrate: 7,
|
||||||
declined: 14,
|
"tautulli-no-matches": 0,
|
||||||
"tautulli-no-matches": 1,
|
|
||||||
"dark-library": 30,
|
|
||||||
};
|
};
|
||||||
const DEFAULT_COOLDOWN = 7;
|
const DEFAULT_COOLDOWN = 0;
|
||||||
|
|
||||||
interface Store {
|
interface Store {
|
||||||
nextId: number;
|
nextId: number;
|
||||||
@@ -205,13 +205,17 @@ export function closeAlert(id: number): Alert | null {
|
|||||||
if (!alert) return null;
|
if (!alert) return null;
|
||||||
|
|
||||||
const cooldownDays = COOLDOWN[alert.category] ?? DEFAULT_COOLDOWN;
|
const cooldownDays = COOLDOWN[alert.category] ?? DEFAULT_COOLDOWN;
|
||||||
const suppressUntil = new Date();
|
let suppressedUntil: string | null = null;
|
||||||
suppressUntil.setDate(suppressUntil.getDate() + cooldownDays);
|
if (cooldownDays > 0) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + cooldownDays);
|
||||||
|
suppressedUntil = d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
alert.status = "closed";
|
alert.status = "closed";
|
||||||
alert.closeReason = "manual";
|
alert.closeReason = "manual";
|
||||||
alert.closedAt = new Date().toISOString();
|
alert.closedAt = new Date().toISOString();
|
||||||
alert.suppressedUntil = suppressUntil.toISOString();
|
alert.suppressedUntil = suppressedUntil;
|
||||||
save(store);
|
save(store);
|
||||||
return toAlert(alert);
|
return toAlert(alert);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user