diff --git a/src/core/UsageStatsService.js b/src/core/UsageStatsService.js index 27d50941..55bf46c4 100644 --- a/src/core/UsageStatsService.js +++ b/src/core/UsageStatsService.js @@ -192,11 +192,24 @@ class UsageStatsService { return record; } - getSnapshot() { + getSnapshot(options = {}) { if (!this.enabled) { return UsageStatsService.createEmptySnapshot(); } + const startTime = Number.isFinite(options.startTime) ? options.startTime : null; + const endTime = Number.isFinite(options.endTime) ? options.endTime : null; + const records = + startTime === null && endTime === null + ? this.records + : this.records.filter(record => { + const recordTime = Date.parse(record.startedAt); + if (!Number.isFinite(recordTime)) return false; + if (startTime !== null && recordTime < startTime) return false; + if (endTime !== null && recordTime > endTime) return false; + return true; + }); + const totalRequests = this.summary.totalRequests; const avgDurationMs = totalRequests > 0 ? Math.round(this.summary.totalDurationMs / totalRequests) : 0; const successRate = @@ -228,8 +241,7 @@ class UsageStatsService { return { accounts, - // Return full request history for display and client-side filtering - records: this.records.slice().reverse(), + records: records.slice().reverse(), startedAt: this.startedAt, summary: { abortedCount: this.summary.abortedCount, diff --git a/src/routes/StatusRoutes.js b/src/routes/StatusRoutes.js index ac62e74a..06567839 100644 --- a/src/routes/StatusRoutes.js +++ b/src/routes/StatusRoutes.js @@ -178,7 +178,23 @@ class StatusRoutes { }); app.get("/api/usage-stats", isAuthenticated, (req, res) => { - const snapshot = this.serverSystem.usageStatsService?.getSnapshot(); + const parseOptionalTime = value => { + if (value === undefined) return null; + if (typeof value !== "string" || !value.trim()) return NaN; + return Date.parse(value); + }; + const startTime = parseOptionalTime(req.query.startTime); + const endTime = parseOptionalTime(req.query.endTime); + + if ( + Number.isNaN(startTime) || + Number.isNaN(endTime) || + (startTime !== null && endTime !== null && startTime > endTime) + ) { + return res.status(400).json({ error: "Invalid usage stats time range" }); + } + + const snapshot = this.serverSystem.usageStatsService?.getSnapshot({ endTime, startTime }); res.json(snapshot || UsageStatsService.createEmptySnapshot()); }); diff --git a/ui/app/pages/StatusPage.vue b/ui/app/pages/StatusPage.vue index 04df8296..65435271 100644 --- a/ui/app/pages/StatusPage.vue +++ b/ui/app/pages/StatusPage.vue @@ -2859,7 +2859,8 @@ const statsState = reactive({ }); // Time range filter: 'all' | '1h' | '6h' | '24h' | '7d' | '30d' | 'custom' -const timeRange = ref("all"); +const DEFAULT_TIME_RANGE = "30d"; +const timeRange = ref(DEFAULT_TIME_RANGE); const customTimeRange = ref([]); const recordFilters = reactive({ apiFormat: [""], @@ -3098,7 +3099,8 @@ const recordFilterOptions = computed(() => { }); const hasActiveStatsFilters = computed( - () => timeRange.value !== "all" || Object.values(recordFilters).some(value => isFilterArrayActive(value)) + () => + timeRange.value !== DEFAULT_TIME_RANGE || Object.values(recordFilters).some(value => isFilterArrayActive(value)) ); // Computed: records filtered by time range + record filters (records are newest-first) @@ -3464,8 +3466,21 @@ const formatAccount = (authIndex, accountName) => { } return `#${authIndex} ${accountName || "N/A"}`; }; +let usageStatsRequestSequence = 0; const fetchUsageStats = async () => { - const res = await fetch("/api/usage-stats"); + const requestSequence = ++usageStatsRequestSequence; + let url = "/api/usage-stats"; + const range = + timeRange.value === "custom" ? normalizedCustomTimeRange.value : buildRelativeTimeRange(timeRange.value); + if (range) { + const params = new URLSearchParams({ + endTime: range[1].toISOString(), + startTime: range[0].toISOString(), + }); + url += `?${params.toString()}`; + } + + const res = await fetch(url); if (res.redirected) { window.location.href = res.url; return; @@ -3479,6 +3494,7 @@ const fetchUsageStats = async () => { } const data = await res.json(); + if (requestSequence !== usageStatsRequestSequence) return; statsState.accounts = data.accounts || []; statsState.records = data.records || []; statsState.startedAt = data.startedAt || null; @@ -3537,7 +3553,7 @@ const showAttemptsDetail = record => { }; const resetRecordFilters = () => { - timeRange.value = "all"; + timeRange.value = DEFAULT_TIME_RANGE; customTimeRange.value = []; recordFilters.apiFormat = [""]; recordFilters.attemptCount = [""]; @@ -3560,6 +3576,12 @@ watch( { flush: "sync" } ); +watch([timeRange, normalizedCustomTimeRange], () => { + fetchUsageStats().catch(err => { + console.error("Error fetching usage stats:", err.message || err); + }); +}); + watch( [() => filteredRecords.value.length, recordsPageSize], () => { @@ -4996,8 +5018,7 @@ onMounted(() => { syncStatsFiltersViewport(statsFiltersMobileMediaQuery); statsFiltersMobileMediaQuery.addEventListener("change", syncStatsFiltersViewport); - updateContent().finally(scheduleUpdate); - fetchUsageStats().finally(scheduleUpdate); + Promise.all([updateContent(), fetchUsageStats()]).finally(scheduleUpdate); // Check for updates once on initial load checkForUpdates();