Build OverSnitch dashboard

Full implementation on top of the Next.js scaffold:

- Leaderboard with per-user request count, storage, avg GB/req, and
  optional Tautulli watch stats (plays, watch hours), each with dense
  per-metric rank (#N/total)
- SWR cache on /api/stats (5-min stale, force-refresh via button);
  client-side localStorage seed so the UI is instant on return visits
- Alerting system: content-centric alerts (unfulfilled downloads,
  partial TV downloads, stale pending requests) and user-behavior
  alerts (ghost requester, low watch rate, declined streak)
- Partial TV detection: flags ended series with <90% of episodes on disk
- Alert persistence in data/alerts.json with open/closed state,
  auto-resolve when condition clears, manual close with per-category
  cooldown, and per-alert notes
- Alert detail page rendered as a server component for instant load
- Dark UI with Tailwind v4, severity-colored left borders, summary
  cards with icons, sortable leaderboard table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 11:13:57 -04:00
parent ef061ea910
commit f871f86284
25 changed files with 2084 additions and 86 deletions
+42
View File
@@ -0,0 +1,42 @@
import { getAlertById, closeAlert, reopenAlert } from "@/lib/db";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const alert = getAlertById(Number(id));
if (!alert) {
return Response.json({ error: "Alert not found" }, { status: 404 });
}
return Response.json(alert);
}
export async function PATCH(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const numId = Number(id);
let body: { status: string };
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
if (body.status === "closed") {
const updated = closeAlert(numId);
if (!updated) return Response.json({ error: "Alert not found" }, { status: 404 });
return Response.json(updated);
}
if (body.status === "open") {
const updated = reopenAlert(numId);
if (!updated) return Response.json({ error: "Alert not found" }, { status: 404 });
return Response.json(updated);
}
return Response.json({ error: "status must be 'open' or 'closed'" }, { status: 400 });
}