Skip to content

feat(alerts): require an HnR rise to persist before notifying - #7

Merged
patrickdundas merged 2 commits into
mainfrom
feat/hnr-sustained-gate
Aug 9, 2026
Merged

feat(alerts): require an HnR rise to persist before notifying#7
patrickdundas merged 2 commits into
mainfrom
feat/hnr-sustained-gate

Conversation

@patrickdundas

Copy link
Copy Markdown
Owner

Problem

TorrentLeech's hit-and-run figure is a live "not currently satisfying" counter, not a permanent strike record. Stale tracker-side leech records age out through it, so the count blips 0 -> 1 -> 0 over a few hours with nothing wrong. checkHnrIncrease fires on every one.

Measured on 11 days of hourly TL polls from tracker_snapshots:

Episode Run length Outcome
Jul 30 5 polls self-cleared
Aug 2 4 polls self-cleared
Aug 6 2 polls self-cleared
Aug 8-9 4 polls self-cleared

Four alerts, four false alarms.

Fix

checkHnrSustained requires an increase to hold for N consecutive polls before it notifies, and fires exactly once — on the poll where the run reaches N. A later poll shifts the rise past the checked offset, so an elevated-but-flat counter stays quiet.

Default N = 6: the smallest value that suppresses all four observed episodes. A genuine hit-and-run is a recorded penalty that never clears, so the only cost is a few hours of notice on something already irreversible.

Notes

  • Configurable per target via thresholds.hnrSustainedPolls, clamped to HNR_SUSTAINED_POLLS_MAX — an over-large value would otherwise exceed the fetched history and silently never fire, which looks identical to "no HnRs".
  • The scheduler loads HNR_HISTORY_POLLS snapshots instead of 1. Only the HnR check reads the extra rows; every other comparison still uses previousSnapshot.
  • Callers supplying no recentHnrs keep the original single-step behaviour.

Tests

18 new cases, including a replay of all four real TL episodes asserting zero alerts, and a non-clearing strike asserting exactly one.

pnpm tsc clean. The one failing suite (chart-transforms) is the known UTC-brittle test — fails in Mountain, passes in CI, untouched here.

TorrentLeech publishes a LIVE "not currently satisfying" counter rather than a
permanent strike record. Stale tracker-side leech records age out through it, so
the count blips 0 -> 1 -> 0 with nothing actually wrong. checkHnrIncrease fires
on every blip, and every one of them is a false alarm.

Measured on 11 days of hourly TL polls: four separate blips, runs of 5, 4, 2 and
4 polls, all self-cleared. checkHnrSustained requires an increase to hold for N
consecutive polls (default 6 - the smallest value that suppresses all four) and
fires exactly once, on the poll where the run completes.

A genuine hit-and-run is a recorded penalty that never clears, so the only cost
is a few hours of notice on something already irreversible.

- N is configurable per target via thresholds.hnrSustainedPolls, clamped to
  HNR_SUSTAINED_POLLS_MAX so an over-large value cannot silently never fire.
- The scheduler now loads HNR_HISTORY_POLLS snapshots instead of one; only the
  HnR check reads the extra rows.
- Callers that supply no history keep the original single-step behaviour.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Knip Code Analysis

Found 9 total issues

Category Count
Unused Dependencies 1
Unused Dev Dependencies 3
Unused Exports 5
View details

Run pnpm knip locally to see the full report.

Use pnpm knip:filter pattern to filter results by file path.


Use /** @public */ JSDoc tags to mark intentionally exported symbols.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🚫 Security audit failed

Critical Failures

Check File Details
External fetch calls have timeouts src/lib/adapters/btn.ts:69 fetch() call without a timeout signal — add signal: AbortSignal.timeout(ms)

Warnings

Check File Details
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:108 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:114 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:175 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:181 catch block swallows error without logging or re-throwing

Passed (36/38)

  • ✅ Auth enforcement on protected routes (per-handler)
  • ✅ No dangerous functions (eval, innerHTML, etc.)
  • ✅ No hardcoded secrets in source
  • ✅ Security headers in next.config.ts
  • ✅ Cookie security (httpOnly, sameSite, secure)
  • ✅ No sensitive fields in API responses
  • ✅ No .env files committed to repo
  • ✅ No raw SQL in API routes
  • ✅ No fetch/redirect with unvalidated URLs in routes
  • ✅ Timing-safe comparison for secret values
  • ✅ No raw SQL migration files (schema-first only)
  • ✅ Docker container runs as non-root user
  • ✅ Public routes match proxy allowlist
  • ✅ File delete operations have path traversal defense
  • ✅ Password hashing uses Argon2 (not SHA-256/bcrypt)
  • ✅ Encrypted columns written via encrypt()
  • ✅ TOTP 2FA flow integrity
  • ✅ Emergency lockdown flow integrity
  • ✅ Scrub & delete (nuke) flow integrity
  • ✅ Backup restore flow integrity
  • ✅ Login flow integrity
  • ✅ Auth result checked before proceeding
  • ✅ Backup password inputs bounded before key derivation
  • ✅ Webhook delivery fetch uses redirect: "error"
  • ✅ SESSION_SECRET minimum-length guard in auth/crypto modules
  • ✅ Notification URL validators include SSRF protection
  • ✅ Dockerfile does not COPY sensitive files
  • ✅ No secret env vars in client components
  • ✅ Adapter Cookie headers guard against injection
  • ✅ Adapter files do not log credential values
  • ✅ No console.log in API routes
  • ✅ No TODO/FIXME in security-critical files
  • ✅ JSON.parse wrapped in try-catch
  • ✅ Request body size validation on upload routes
  • ✅ BigInt fields use string serialization
  • ✅ No raw error messages in API responses

Summary: 36/38 checks passed
1 critical failure(s)
— 1 warning(s)

See scripts/security-audit.ts for check definitions and SECURITY.md for the full security architecture.

Trivy flagged CVE-2026-67213 (HIGH) against nanoid 3.3.16, reached transitively
through postcss, which is itself pulled in by next, @tailwindcss/postcss and
vite. The image build gates on the scan, so this blocked release.

Pinned to ^3.3.17 rather than the >=3.3.17 that a naive read of the advisory
suggests: the open range resolves to nanoid 6.x, which is a major version away
from the ^3.3.11 postcss actually asks for. Staying inside 3.x takes the fix
without swapping a scanner finding for a runtime break.

Unrelated to the HnR work on this branch; it surfaced because this is the first
build since the advisory landed.
@patrickdundas
patrickdundas merged commit 7cac42d into main Aug 9, 2026
6 checks passed
@patrickdundas
patrickdundas deleted the feat/hnr-sustained-gate branch August 9, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant