From 9b4100f7d6665a6fa7e6274881a6473e4541feb5 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 6 May 2026 21:15:37 +0200 Subject: [PATCH] fix(web): unify dashboard pagination + bump fleet-health page size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard: removed the standalone "Load 100 more sessions..." button. The session table now has a single Prev/Next surface that transparently fetches the next server batch when the user pages past locally-loaded sessions — the existing auto-load effect in page.tsx already drives loadMore() once currentPage * sessionsPerPage exceeds sessions.length, but handleNextPage was clamping to totalPages so currentPage could never overshoot, leaving "Next" disabled until the user clicked the separate Load-More button. - useDashboardFilters now takes hasMore + loadingMore; handleNextPage advances past totalPages when hasMore && !loadingMore. A snap-back effect resets currentPage to totalPages once a load finishes that didn't grow totalPages (filter case: new batch yielded zero matches). - SessionTable renders the Prev/Next controls whenever there's somewhere to navigate (totalPages > 1 || hasMore), shows "Page X of Y+" while more is on the server, disables Next only at the true end, and shows an inline spinner inside Next during loadingMore. The empty tbody distinguishes "loading sessions..." from "no results". Fleet Health: bumped FLEET_PAGE_SIZE from 200 to 1000 (backend MaxPageSize is 1000). A 90-day window on a busy install now drains in ~1/5 the round-trips, eliminating the 5x roundtrip-amplification users observed on initial load. Type-check clean, vitest 249 passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/dashboard/components/SessionTable.tsx | 80 ++++++------------- .../dashboard/hooks/useDashboardFilters.ts | 23 +++++- .../app/dashboard/page.tsx | 3 +- .../app/fleet-health/page.tsx | 4 +- 4 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx b/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx index 0b01a0fb..dee36ff1 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx @@ -71,7 +71,6 @@ interface SessionTableProps { onSessionsPerPageChange: (value: number) => void; hasMore: boolean; loadingMore: boolean; - onLoadMore: () => void; onLoadAll: () => void; adminMode: boolean; globalAdminMode: boolean; @@ -109,7 +108,6 @@ export function SessionTable({ onSessionsPerPageChange, hasMore, loadingMore, - onLoadMore, onLoadAll, adminMode, globalAdminMode, @@ -722,7 +720,17 @@ export function SessionTable({ {paginatedSessions.length === 0 ? ( - No sessions found matching your search. + {loadingMore ? ( + + + + + + Loading sessions... + + ) : ( + "No sessions found matching your search." + )} ) : ( @@ -752,75 +760,39 @@ export function SessionTable({ - {/* Pagination Controls */} - {totalPages > 1 && ( + {/* Pagination Controls — single Prev/Next surface that transparently fetches + the next server batch when the user pages past locally-loaded sessions. + Renders whenever there's somewhere to navigate (more local pages OR more + on the server). */} + {(totalPages > 1 || hasMore) && (
- Page {currentPage} of {totalPages} ({sortedSessions.length}{hasMore ? '+' : ''} total sessions) + Page {currentPage} of {totalPages}{hasMore ? '+' : ''} ({sortedSessions.length}{hasMore ? '+' : ''} total sessions)
)} - - {/* Load More Button */} - {hasMore && ( -
- {searchQuery.trim() ? ( - // Search active: offer to load all remaining sessions - - ) : ( - // No search: standard load-more (100 at a time) - - )} -
- )} ); } diff --git a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts index 92fd53f8..4dbb2de2 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts +++ b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts @@ -13,6 +13,8 @@ interface UseDashboardFiltersParams { tenantId: string | null | undefined; globalAdminMode: boolean; tenantIdFilter: string; + hasMore: boolean; + loadingMore: boolean; } interface DashboardStats { @@ -57,6 +59,8 @@ export function useDashboardFilters({ tenantId, globalAdminMode, tenantIdFilter, + hasMore, + loadingMore, }: UseDashboardFiltersParams): UseDashboardFiltersReturn { const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState(null); @@ -213,7 +217,24 @@ export function useDashboardFilters({ }, [sortedSessions, currentPage, sessionsPerPage]); const handlePreviousPage = () => setCurrentPage((prev) => Math.max(1, prev - 1)); - const handleNextPage = () => setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + // Allow advancing past totalPages when the server has more pages; the auto-load + // effect in page.tsx will fetch the next batch and totalPages catches up. Without + // this overshoot the user could never trigger a load while staying inside a single + // Prev/Next control — they would have to click a separate "Load more" button. + const handleNextPage = () => setCurrentPage((prev) => { + if (prev < totalPages) return prev + 1; + if (hasMore && !loadingMore) return prev + 1; + return prev; + }); + + // Snap currentPage back to the last valid page once a load finishes that did NOT + // grow totalPages (e.g. an active filter narrowed the new batch to zero matches). + // Without this the user would be stuck on an empty paginated slice with no way + // to recover except clicking Prev. + useEffect(() => { + if (loadingMore) return; + if (totalPages > 0 && currentPage > totalPages) setCurrentPage(totalPages); + }, [currentPage, totalPages, loadingMore]); return { searchQuery, setSearchQuery, diff --git a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx index 3be9f249..5c3c04c7 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx @@ -66,6 +66,8 @@ export default function Home() { tenantId, globalAdminMode, tenantIdFilter, + hasMore, + loadingMore, }); // Redirect regular users (non-admin, non-operator) to progress portal – they must never see the session list @@ -275,7 +277,6 @@ export default function Home() { onSessionsPerPageChange={handleSessionsPerPageChange} hasMore={hasMore} loadingMore={loadingMore} - onLoadMore={loadMore} onLoadAll={loadAll} adminMode={adminMode} globalAdminMode={globalAdminMode} diff --git a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx index ce3be228..7e716bdd 100644 --- a/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx +++ b/src/Web/autopilot-monitor-web/app/fleet-health/page.tsx @@ -14,7 +14,9 @@ import FleetStatCard from "./components/FleetStatCard"; import { Session } from "@/types"; import { useAdminMode } from "@/hooks/useAdminMode"; -const FLEET_PAGE_SIZE = 200; +// Backend caps pageSize at 1000 — use the maximum so a 90-day window on a busy +// install (10k+ sessions) drains in a handful of round-trips instead of 50+. +const FLEET_PAGE_SIZE = 1000; interface AppMetric { appName: string;