"use client"; import EmailReportControl from "./EmailReportControl"; import { useCallback, useEffect, useRef, 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"; import "./reports.css"; type Change = { current: number; previous: number; difference: number; percent: number | null }; type MonthlyReport = Omit & { month: string; available_months: string[]; is_partial: boolean; comparison_capped: boolean; period_start: string; period_end: string; comparison_month: string; comparison_start: string; comparison_end: string; previous_summary?: Stats["summary"]; previous_requests?: Omit; changes?: Record<"minutes" | "movies" | "episodes" | "plays" | "active_days" | "longest_streak" | "requests", Change>; }; const monthLabel = (month: string, short = false) => new Date(`${month}-01T00:00:00Z`).toLocaleDateString(undefined, { month: short ? "short" : "long", year: "numeric", timeZone: "UTC", }); const decimal = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 1 }); function ChangeLabel({ change, unit = "" }: { change: Change; unit?: string }) { const delta = change.difference; return (
0 ? "is-up" : delta < 0 ? "is-down" : "is-flat"}`}> {delta === 0 ? "No change" : `${delta > 0 ? "+" : "−"}${decimal(Math.abs(delta))}${unit}${change.percent === null ? "" : ` (${delta > 0 ? "+" : "−"}${decimal(Math.abs(change.percent))}%)`}`} {change.percent === null ? "No activity recorded in the comparison period" : `Previously ${decimal(change.previous)}${unit}`}
); } export default function MonthlyReportsPage() { const router = useRouter(); const [month, setMonth] = useState(""); const [monthReady, setMonthReady] = useState(false); const [months, setMonths] = useState([]); 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 [downloading, setDownloading] = useState(false); const [downloadError, setDownloadError] = useState(""); const downloadController = useRef(null); useEffect(() => { setMonth(new URLSearchParams(window.location.search).get("month") || ""); setMonthReady(true); }, []); useEffect(() => { if (monthReady) window.history.replaceState(null, "", `/insights/reports${month ? `?month=${encodeURIComponent(month)}` : ""}`); }, [month, monthReady]); useEffect(() => () => downloadController.current?.abort(), []); const load = useCallback( async (signal: AbortSignal) => { setBusy(true); setError(""); setData(null); setDownloadError(""); try { const query = month ? `?month=${encodeURIComponent(month)}` : ""; const response = await authFetch(`${getApiBase()}/insights/reports/monthly${query}`, { signal }); if (response.status === 401) { router.replace(`/login?next=${encodeURIComponent(`/insights/reports${query}`)}`); return; } if (response.status === 403) throw new Error("Your account cannot access viewing reports. Please contact an administrator."); if (!response.ok) { const result = await response.json().catch(() => ({})); throw new Error( typeof result.detail === "string" ? result.detail : "Your report is temporarily unavailable. Please try again shortly.", ); } const result = (await response.json()) as MonthlyReport; if (!signal.aborted) { setData(result); setMonths(result.available_months); } } catch (err) { if (!signal.aborted) setError(err instanceof Error ? err.message : "Could not load your report."); } finally { if (!signal.aborted) setBusy(false); } }, [month, router], ); useEffect(() => { void revision; if (!monthReady) return; const controller = new AbortController(); void load(controller.signal); return () => controller.abort(); }, [load, revision, monthReady]); const download = async () => { if (data?.state !== "ready" || downloading) return; const selected = data.month; const controller = new AbortController(); downloadController.current = controller; setDownloading(true); setDownloadError(""); try { const response = await authFetch(`${getApiBase()}/insights/reports/monthly.csv?month=${selected}`, { signal: controller.signal, }); if (response.status === 401) { router.replace(`/login?next=${encodeURIComponent(`/insights/reports?month=${selected}`)}`); return; } if (!response.ok) throw new Error("The report could not be downloaded. Please try again."); const blob = await response.blob(); if (controller.signal.aborted) return; const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `magent-monthly-report-${selected}.csv`; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1000); } catch (err) { if (!controller.signal.aborted) setDownloadError(err instanceof Error ? err.message : "Could not download your report."); } finally { if (!controller.signal.aborted) setDownloading(false); } }; const selectedMonth = month || data?.month || ""; const monthIndex = months.indexOf(selectedMonth); const summary = data?.summary; const changes = data?.changes; return (
} />

From Jellystat · UTC

{downloadError && (

{downloadError}

)} {data?.state === "ready" && !busy && } {busy && (

Putting your month together

Gathering your viewing history and the previous month’s comparison.

)} {error && (

Report couldn’t load

{error}

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

Your monthly story starts here

{isAdmin ? "Connect Jellystat to bring your monthly viewing reports into Magent." : "Monthly reports will appear once your administrator connects Jellystat."}

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

Link your viewing account

Your report needs your Jellyfin account link. Sign in using Jellyfin, or ask your administrator to review your user identities.

{isAdmin && ( Review user identities )}
)} {data && summary && changes && ( <>
{data.is_partial ? "Month to date" : "Your monthly recap"}

{monthLabel(data.month)}

{data.is_partial ? `Compared with the same elapsed time in ${monthLabel(data.comparison_month)}${data.comparison_capped ? ", capped at the end of that month" : ""}.` : `Compared with ${monthLabel(data.comparison_month)}.`}

{data.is_partial ? "In progress" : "Complete month"} {data.updated_at && `Updated ${new Date(data.updated_at).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC`}
Minutes watched {number(summary.minutes)} {decimal(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)} {data.requests.movies} movies · {data.requests.tv} TV requests
{summary.plays === 0 && (
No viewing history was recorded for this month. Your request totals and comparison are still shown.
)}

Your viewing habits

{summary.active_days} days
Days you watched

At least one minute of viewing.

{summary.longest_streak} days
Longest run

Consecutive viewing days this month.

{data.daily?.length ? number(summary.minutes / data.daily.length) : 0} min
Daily average

Across the calendar days in this report.

{data.patterns && ( <>
Average play {decimal(data.patterns.average_play_minutes)} min

Time per recorded playback session.

Longest play {decimal(data.patterns.longest_play_minutes)} min

Your longest recorded session this month.

Weekend viewing {decimal(data.patterns.weekend_percent)} %

Share of viewing on Saturday and Sunday (UTC).

)}

Most watched

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

Your most watched titles will appear here.

)}

A look back

Latest 20 plays this month
{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 during this month will appear here.

)}

Your requests

View all
{data.requests.total} submitted in {monthLabel(data.month, true)}
{data.requests.pending} Pending {data.requests.approved} Approved {data.requests.declined} Declined
{data.requests.recent.length ? ( ) : (

No requests recorded during this month.

)}

Statuses reflect where these requests are now.

Private to your account · Based on history retained by Jellystat and requests available in Magent. Plays include unfinished watches. Dates and streaks use UTC. Reports may take a minute to refresh; historical totals can change when retained history or library metadata changes.

)}
); }