"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { authFetch, getApiBase } from "../lib/auth"; import { useEffectiveRole } from "../lib/viewMode"; import PageHeading from "../ui/PageHeading"; import { type Stats, BreakdownCard, RecentArtwork, StatsNavigation, StreamingCard, ViewingChart, dateLabel, number, } from "./components"; import "./stats.css"; export default function InsightsPage() { const router = useRouter(); const [days, setDays] = useState(30); const [data, setData] = useState(null); const isAdmin = useEffectiveRole(data?.is_admin ? "admin" : "user") === "admin"; const [busy, setBusy] = useState(true); const [error, setError] = useState(""); const [revision, setRevision] = useState(0); const load = useCallback( async (signal: AbortSignal) => { setBusy(true); setError(""); setData(null); try { const response = await authFetch(`${getApiBase()}/insights?days=${days}`, { signal }); if (response.status === 401) { router.replace("/login?next=%2Finsights"); return; } if (response.status === 403) throw new Error("Your account cannot access viewing stats. Please contact an administrator."); if (!response.ok) { const result = await response.json().catch(() => ({})); throw new Error( typeof result.detail === "string" ? result.detail : "Your viewing stats are temporarily unavailable. Please try again shortly.", ); } const result = (await response.json()) as Stats; if (!signal.aborted) setData(result); } catch (err) { if (!signal.aborted) setError(err instanceof Error ? err.message : "Could not load your stats."); } finally { if (!signal.aborted) setBusy(false); } }, [days, router], ); useEffect(() => { void revision; const controller = new AbortController(); void load(controller.signal); return () => controller.abort(); }, [load, revision]); const summary = data?.summary; return (
setRevision((value) => value + 1)} > {busy ? "Loading…" : "Refresh stats"} } />
Stats period {[7, 30, 90, 365].map((value) => ( ))}

From Jellystat {data?.updated_at && ( {" "} · Updated{" "} {new Date(data.updated_at).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} )}

{busy && (

Gathering your stats

Fetching your viewing history from Jellystat.

)} {error && (

Stats couldn’t load

{error}

)} {data?.state === "not_configured" && (

Your viewing story starts here

{isAdmin ? "Connect your Jellystat instance to bring personal viewing stats into Magent." : "Viewing stats will appear here once your administrator connects Jellystat."}

{isAdmin && ( Connect Jellystat )}
)} {data?.state === "unlinked" && (

Link your viewing account

Your Magent account needs a Jellyfin identity to find your stats. Sign in using Jellyfin, or ask your administrator to sync Jellyfin users.

)} {summary && ( <>
Minutes watched {number(summary.minutes)} {number(summary.minutes / 60)} hours across {number(summary.plays)} plays
Movies played {number(summary.movies)} Different movies you pressed play on
Episodes played {number(summary.episodes)} Different episodes in your history
Requests made {number(data.requests.total)} {number(data.requests.movies)} movies · {number(data.requests.tv)} TV requests
{summary.plays === 0 && (
No viewing history in this period yet. Try a longer period, or come back after your next watch.
)}

A little watch history

{summary.current_streak} days
Current streak

Consecutive viewing days through today or yesterday.

{summary.longest_streak} days
Longest run

Your best streak in this period.

{summary.active_days} days
Time for a story

Days with at least a minute watched.

Most watched

By minutes
{data.top_titles?.length ? (
    {data.top_titles.map((title, index) => (
  1. {String(index + 1).padStart(2, "0")}
    {title.title} {title.type === "series" ? "TV series" : title.type === "movie" ? "Movie" : "Other media"} ·{" "} {title.plays} plays
    {number(title.minutes)} min
  2. ))}
) : (

Your favourites will find their place here.

)}
)} {data && (
{summary && (

Recently watched

Latest 20 plays
{data.recent?.length ? (
{data.recent.map((play) => (
{play.series || play.title} {play.series ? `${play.episode} · ${play.title}` : play.type === "movie" ? "Movie" : "Media"} {play.client} · {play.method}
{number(play.minutes)} min
))}
) : (

Plays recorded by Jellystat will appear here.

)}
)}

Your requests

View all
{data.requests.total} submitted in the past {days} days
{data.requests.pending} Pending {data.requests.approved} Approved {data.requests.declined} Declined
{data.requests.recent.length > 0 ? ( ) : (

Something on your watchlist? Make a request.

)}
)} {summary && (

Private to your account · Stats come from Jellystat and may take a minute to refresh. Counts describe plays, including unfinished watches. Movies are identified from Jellystat’s movie libraries; other media still contributes to watch time. Charts and streaks use UTC and the activity dates recorded by Jellystat.

)}
); }