From c4e67effc32ea0bceb9f50eed8b72b19a0de0002 Mon Sep 17 00:00:00 2001 From: zZebrahz Date: Sun, 7 Jun 2026 16:59:10 -0700 Subject: [PATCH] fix(admin): correct dashboard library counts and scans (#78) * fix(admin): correct dashboard library counts and scans * fix(admin): handle mixed library dashboard counts * fix(admin): refine dashboard count affordances --- internal/api/handlers/admin_stats.go | 52 ++++++-- web/src/api/types.ts | 2 + web/src/pages/AdminDashboard.tsx | 175 ++++++++++++++++++++------- 3 files changed, 179 insertions(+), 50 deletions(-) diff --git a/internal/api/handlers/admin_stats.go b/internal/api/handlers/admin_stats.go index 47b7840b..0042dc64 100644 --- a/internal/api/handlers/admin_stats.go +++ b/internal/api/handlers/admin_stats.go @@ -22,7 +22,9 @@ type AdminStats struct { TotalFiles int `json:"total_files"` TotalUsers int `json:"total_users"` TotalMovies int `json:"total_movies"` + TotalMovieFiles int `json:"total_movie_files"` TotalShows int `json:"total_shows"` + TotalShowFiles int `json:"total_show_files"` ActiveStreams int `json:"active_streams"` TotalStorageBytes int64 `json:"total_storage_bytes"` WatchProviderActivity WatchProviderActivity `json:"watch_provider_activity"` @@ -127,13 +129,15 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error } var ( - totalUsers int64 - totalItems int64 - totalFiles int64 - totalMovies int64 - totalShows int64 - activeStreams int64 - totalStorage int64 + totalUsers int64 + totalItems int64 + totalFiles int64 + totalMovies int64 + totalMovieFiles int64 + totalShows int64 + totalShowFiles int64 + activeStreams int64 + totalStorage int64 ) row := pool.QueryRow(ctx, ` @@ -151,8 +155,34 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error file_stats AS ( SELECT COUNT(*)::bigint AS total_files, + COUNT(*) FILTER (WHERE file_kind = 'movie')::bigint AS total_movie_files, + COUNT(*) FILTER (WHERE file_kind = 'series')::bigint AS total_show_files, COALESCE(SUM(file_size), 0)::bigint AS total_storage_bytes - FROM media_files + FROM ( + SELECT + media_files.file_size, + CASE + WHEN lower(trim(COALESCE(NULLIF(media_items.type, ''), ''))) = 'movie' + THEN 'movie' + WHEN lower(trim(COALESCE(NULLIF(media_items.type, ''), ''))) = 'series' + THEN 'series' + WHEN episodes.content_id IS NOT NULL + THEN 'series' + WHEN lower(trim(COALESCE(NULLIF(media_files.base_type, ''), ''))) IN ('movie', 'movies') + THEN 'movie' + WHEN lower(trim(COALESCE(NULLIF(media_files.base_type, ''), ''))) IN ('series', 'tv', 'show', 'shows', 'tvshows') + THEN 'series' + WHEN lower(trim(media_folders.type)) IN ('movie', 'movies') + THEN 'movie' + WHEN lower(trim(media_folders.type)) IN ('series', 'tv', 'show', 'shows', 'tvshows') + THEN 'series' + ELSE '' + END AS file_kind + FROM media_files + JOIN media_folders ON media_folders.id = media_files.media_folder_id + LEFT JOIN media_items ON media_items.content_id = media_files.content_id + LEFT JOIN episodes ON episodes.content_id = media_files.episode_id + ) classified_files ), session_stats AS ( SELECT COUNT(*)::bigint AS active_streams @@ -163,7 +193,9 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error item_stats.total_items, file_stats.total_files, item_stats.total_movies, + file_stats.total_movie_files, item_stats.total_shows, + file_stats.total_show_files, session_stats.active_streams, file_stats.total_storage_bytes FROM user_stats @@ -176,7 +208,9 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error &totalItems, &totalFiles, &totalMovies, + &totalMovieFiles, &totalShows, + &totalShowFiles, &activeStreams, &totalStorage, ); err != nil { @@ -194,7 +228,9 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error TotalItems: int(totalItems), TotalFiles: int(totalFiles), TotalMovies: int(totalMovies), + TotalMovieFiles: int(totalMovieFiles), TotalShows: int(totalShows), + TotalShowFiles: int(totalShowFiles), ActiveStreams: int(activeStreams), TotalStorageBytes: totalStorage, WatchProviderActivity: activity, diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 61c7cc48..738c843e 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -2049,7 +2049,9 @@ export interface AdminStats { total_files: number; total_users: number; total_movies: number; + total_movie_files?: number; total_shows: number; + total_show_files?: number; active_streams: number; total_storage_bytes: number; watch_provider_activity: WatchProviderActivity; diff --git a/web/src/pages/AdminDashboard.tsx b/web/src/pages/AdminDashboard.tsx index f9c1cd24..9e83b157 100644 --- a/web/src/pages/AdminDashboard.tsx +++ b/web/src/pages/AdminDashboard.tsx @@ -1,14 +1,17 @@ import type { ReactNode } from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Link, useNavigate } from "react-router"; import { AdminSessionActions } from "@/components/AdminSessionActions"; +import { useEventChannel } from "@/components/realtimeEventsContext"; import { fetchAdminStats, useAdminStats, useAdminSessions } from "@/hooks/queries/admin/stats"; import { useAdminUsers } from "@/hooks/queries/admin/users"; import { useAdminLibraries, + useCancelLibraryScans, useScanAllLibraries, useScanLibrary, } from "@/hooks/queries/admin/libraries"; +import { useActiveScans } from "@/hooks/queries/admin/scans"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -31,6 +34,7 @@ import { Pause, Library, ScanLine, + Square, } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import type { @@ -38,16 +42,35 @@ import type { AdminStats, Library as LibraryType, AdminUser, + ScanRun, WatchProviderActivity, } from "@/api/types"; import { useQueryClient } from "@tanstack/react-query"; import { adminKeys } from "@/hooks/queries/keys"; import { usePageActivity } from "@/hooks/usePageActivity"; +import { cn } from "@/lib/utils"; +import { compareActiveScans, formatActiveScanMode, formatActiveScanProgress } from "@/lib/scanRuns"; const REFRESH_SPINNER_MIN_VISIBLE_MS = 1_000; const DASHBOARD_AUTO_REFRESH_MS = 60_000; const RELATIVE_UPDATED_LABEL_TICK_MS = 30_000; +function formatFileCount(count: number | null | undefined) { + if (count == null) { + return "—"; + } + return count === 1 ? "1 file" : `${count.toLocaleString()} files`; +} + +function formatDashboardLibraryScanProgress(scan: ScanRun, activeScanCount: number) { + const status = scan.status === "running" ? "Scanning" : "Queued"; + const progress = formatActiveScanProgress(scan); + const detail = + progress || (scan.status === "running" ? formatActiveScanMode(scan) : "Waiting for capacity"); + const extraScans = activeScanCount > 1 ? ` + ${activeScanCount - 1} more` : ""; + return `${status}: ${detail}${extraScans}`; +} + export default function AdminDashboard() { const queryClient = useQueryClient(); const statsQuery = useAdminStats(); @@ -222,6 +245,7 @@ export default function AdminDashboard() { +
-
- -
-
-
- )) + ); + }) )}