diff --git a/artifacts/api-server/src/routes/stats.ts b/artifacts/api-server/src/routes/stats.ts index 68fcff6e..9fa192f7 100644 --- a/artifacts/api-server/src/routes/stats.ts +++ b/artifacts/api-server/src/routes/stats.ts @@ -139,11 +139,23 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise< let rangeEndExclusive: Date; let rangeDays: number; - if (range === "custom" && fromParam && toParam) { + if (range === "custom") { + if (!fromParam || !toParam) { + res.status(400).json({ + error: "Missing from/to: 'range=custom' requires both 'from' and 'to' as YYYY-MM-DD dates", + }); + return; + } const fromDate = parseUtcDate(fromParam); const toDate = parseUtcDate(toParam); - if (!fromDate || !toDate || fromDate.getTime() > toDate.getTime()) { - res.status(400).json({ error: "Invalid from/to" }); + if (!fromDate || !toDate) { + res.status(400).json({ + error: "Invalid from/to: expected YYYY-MM-DD dates", + }); + return; + } + if (fromDate.getTime() > toDate.getTime()) { + res.status(400).json({ error: "Invalid from/to: 'from' must be on or before 'to'" }); return; } const MAX_DAYS = 366; @@ -156,7 +168,6 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise< rangeEndExclusive = new Date(toDate.getTime() + DAY_MS); rangeDays = days; } else { - if (range === "custom") range = "7d"; rangeDays = range === "90d" ? 90 : range === "30d" ? 30 : 7; rangeStart = new Date(startOfTodayUtc.getTime() - (rangeDays - 1) * DAY_MS); rangeEndExclusive = new Date(startOfTodayUtc.getTime() + DAY_MS); diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 25e0dd19..011b1ac5 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -298,7 +298,8 @@ "from": "من", "to": "إلى", "apply": "تطبيق", - "invalid": "اختر نطاقًا صحيحًا" + "invalid": "اختر نطاقًا صحيحًا", + "loadError": "تعذّر تحميل الإحصائيات للنطاق المحدّد. يرجى التحقق من التواريخ والمحاولة مرة أخرى." }, "topApps": "أكثر التطبيقات استخدامًا", "topAppsSubtitle": "حسب الفتح في {{range}}", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index c6ac0145..63ebfa8c 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -295,7 +295,8 @@ "from": "From", "to": "To", "apply": "Apply", - "invalid": "Pick a valid date range" + "invalid": "Pick a valid date range", + "loadError": "Couldn't load stats for the selected dates. Please check the range and try again." }, "topApps": "Top apps", "topAppsSubtitle": "By opens in {{range}}", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 32899c8f..c8344c09 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -24,7 +24,7 @@ import { getGetAdminStatsQueryKey, useAdminIssueResetLink, } from "@workspace/api-client-react"; -import type { App, Service, UserProfile, AppSettings, AdminStats } from "@workspace/api-client-react"; +import type { App, Service, UserProfile, AppSettings, AdminStats, GetAdminStatsQueryError } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { useAuth } from "@/contexts/AuthContext"; import { useUpload, type UploadResponse } from "@workspace/object-storage-web"; @@ -149,12 +149,18 @@ export default function AdminPage() { statsRange === "custom" ? { range: "custom" as const, from: appliedCustom.from, to: appliedCustom.to } : { range: statsRange }; - const { data: adminStats } = useGetAdminStats(statsQueryParams, { + const { data: adminStats, error: adminStatsError } = useGetAdminStats(statsQueryParams, { query: { queryKey: getGetAdminStatsQueryKey(statsQueryParams), enabled: isAdmin && (statsRange !== "custom" || isCustomValid), + retry: false, }, }); + const adminStatsErrorMessage: string | null = (() => { + if (!adminStatsError) return null; + const err = adminStatsError as GetAdminStatsQueryError; + return err.data?.error ?? err.message ?? null; + })(); const createApp = useCreateApp(); const updateApp = useUpdateApp(); @@ -499,6 +505,7 @@ export default function AdminPage() { users={users} appSettings={appSettings} adminStats={adminStats} + adminStatsErrorMessage={adminStatsErrorMessage} lang={lang} statsRange={statsRange} onStatsRangeChange={setStatsRange} @@ -963,6 +970,7 @@ function DashboardSection({ users, appSettings, adminStats, + adminStatsErrorMessage, lang, statsRange, onStatsRangeChange, @@ -979,6 +987,7 @@ function DashboardSection({ users: UserProfile[] | undefined; appSettings: AppSettings | undefined; adminStats: AdminStats | undefined; + adminStatsErrorMessage: string | null; lang: string; statsRange: "7d" | "30d" | "90d" | "custom"; onStatsRangeChange: (range: "7d" | "30d" | "90d" | "custom") => void; @@ -1220,6 +1229,17 @@ function DashboardSection({ )} )} + {adminStatsErrorMessage && ( +