feat(admin): mercury date picker, model mappings page - #1808
Conversation
- replace date range pickers in apps/ui and ee/admin with mercury-style popover (presets + custom month-range calendar) - migrate admin from ?range= to ?from=/?to= url params; extend api to accept from/to for providers, models, and timeseries - add 15m/12h/2d/7d windows to history charts; parent window picker on model detail page controls all charts + stats cards - add /model-provider-mappings admin page with sortable table; fix request counts by reading from projectHourlyModelStats instead of modelProviderMapping.logsCount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (3)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces explicit Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ee/admin/src/components/history-chart.tsx (1)
24-36:⚠️ Potential issue | 🔴 CriticalRemove "30m" from the
HistoryWindowtype or add it back to thewindowOptionsarray.The type definition (line 29) includes
"30m", but thewindowOptionsarray (lines 68-80) omits it. This inconsistency creates a mismatch where TypeScript allows"30m"as valid, but the UI doesn't offer it as a selectable option. Additionally,"30m"is actively used inapps/api/src/routes/admin.tsandapps/ui/src/components/date-range-select.tsx, and remains in the API type definitions, suggesting the value may still be needed. Either remove it from the type entirely or restore it to the options array.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/components/history-chart.tsx` around lines 24 - 36, The HistoryWindow union includes "30m" but the windowOptions array omits it, causing a type/UI mismatch; fix by either removing "30m" from the HistoryWindow type or (preferred since other code references it) add "30m" back into the windowOptions array so the UI can select it — update the windowOptions array in history-chart.tsx to include the "30m" entry (matching label/value format used for other options) and verify related usages (e.g., any code that consumes HistoryWindow such as date-range-select and admin route handlers) still accept the value so API and UI remain consistent.
🧹 Nitpick comments (8)
apps/ui/src/components/date-range-select.tsx (1)
175-180: Consider adding a placeholder to the search input for better UX.The search input has no placeholder text, which may leave users unsure of its purpose.
💡 Suggested improvement
<Input autoFocus value={search} onChange={(e) => setSearch(e.target.value)} + placeholder="Search time ranges..." className="h-8 rounded-none border-0 border-b-2 border-primary bg-transparent px-0 shadow-none focus-visible:ring-0" />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/date-range-select.tsx` around lines 175 - 180, The search Input component currently has no placeholder which can confuse users; update the Input (the JSX element named Input that uses value={search} and onChange={(e) => setSearch(e.target.value)}) to include a descriptive placeholder prop (e.g., placeholder="Search…" or "Filter by name/date") so users understand its purpose, keeping the existing value and onChange logic intact and ensuring accessibility by choosing concise, user-friendly text.ee/admin/src/components/date-range-picker.tsx (1)
1-468: Significant code duplication withapps/ui/src/components/date-range-picker.tsx.This file shares ~90% of its code with the apps/ui version, including
buildPresets(),findMatchingPreset(),MonthRangePicker, and most UI logic. Key differences:
Aspect ee/admin apps/ui Props None buildUrl,pathDefault range 29 days 6 days URL strategy Uses pathnamedirectlyUses buildUrl()Popover align endstartConsider extracting the shared logic (presets,
MonthRangePicker,findMatchingPreset, etc.) to a shared UI package to reduce maintenance burden and ensure consistent behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/components/date-range-picker.tsx` around lines 1 - 468, This file duplicates most of the date-range logic—extract shared code into the UI package: move MONTH_NAMES, getQuarterLabel, buildPresets, findMatchingPreset, getDateRangeFromParams (but make default-range configurable), compareMonth, and MonthRangePicker into apps/ui so both consumers import them; update DateRangePicker to import those helpers and only keep plumbing/UI-specific bits (props like buildUrl/path, defaultRangeDays, and popover align) in the consumer components; ensure MonthRangePicker accepts props for today/disabled logic if needed and parameterize buildPresets or getDateRangeFromParams to allow different default ranges (6 vs 29 days) and let the Popover align be passed in from the caller (align="end" vs "start") so behavior can be customized without duplicating code.apps/ui/src/components/date-range-picker.tsx (1)
340-344: Presets may become stale if the component stays mounted across midnight.
buildPresets()capturestodayat call time, but theuseMemohas an empty dependency array. If the component remains mounted past midnight, the presets will reflect the original day's dates.💡 Consider adding a date-based dependency
-const presets = useMemo(() => buildPresets(), []); +const todayKey = format(new Date(), "yyyy-MM-dd"); +const presets = useMemo(() => buildPresets(), [todayKey]);This ensures presets recalculate when the date changes. For admin dashboards where sessions may span days, this prevents confusing behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/date-range-picker.tsx` around lines 340 - 344, The presets are built once via useMemo(() => buildPresets(), []) so they become stale after midnight; update the memo to depend on a day-derived value (e.g., a currentDate or start-of-day value) so buildPresets() is re-run when the calendar day changes, and ensure the activePreset useMemo (which calls findMatchingPreset(from, to, presets)) uses the same date-derived dependency alongside from/to so matching stays in sync across midnight.ee/admin/src/components/model-detail-client.tsx (1)
17-19: Consider extracting sharedformatNumberhelper.This same function exists in
model-provider-charts.tsx(line 14-16). Consider extracting it to a shared utility (e.g.,@/lib/utilsor@/lib/format) to reduce duplication.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/components/model-detail-client.tsx` around lines 17 - 19, The helper function formatNumber in model-detail-client.tsx is duplicated in model-provider-charts.tsx; extract it to a shared utility (e.g., create a new module like lib/format or lib/utils) and export it, then replace the local definitions in both model-detail-client.tsx and model-provider-charts.tsx with an import of the shared formatNumber function; ensure the exported function signature (formatNumber(n: number)): string and its Intl.NumberFormat("en-US") behavior are preserved and update imports accordingly.ee/admin/src/app/models/page.tsx (1)
70-70: Consider encodingfromandtovalues in URL.The
dateParamsstring directly interpolatesfromandtowithoutencodeURIComponent. While date strings (YYYY-MM-DD format) typically don't contain special characters, encoding would be safer for consistency with howsearchis handled on line 80.🔧 Suggested fix
- const dateParams = from && to ? `&from=${from}&to=${to}` : ""; + const dateParams = from && to ? `&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` : "";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/app/models/page.tsx` at line 70, The dateParams string currently interpolates from and to raw into the URL (dateParams = from && to ? `&from=${from}&to=${to}` : ""), which can break if values contain special characters; update the construction of dateParams to encode both values using encodeURIComponent (e.g., encodeURIComponent(from) and encodeURIComponent(to)) so it matches how search is handled and ensures safe URL encoding for the dateParams variable.ee/admin/src/app/model-provider-mappings/page.tsx (1)
168-169: Type casts rely on server-side validation.The
as MappingSortByandas SortOrdercasts trust that query parameters match expected values. While the API validates these via its zod schema, consider adding client-side validation for better type safety and immediate feedback.♻️ Optional: Add validation for type safety
+const validSortBy: MappingSortBy[] = [ + "providerId", + "modelId", + "logsCount", + "errorsCount", + "avgTimeToFirstToken", + "updatedAt", +]; +const validSortOrder: SortOrder[] = ["asc", "desc"]; + -const sortBy = (params?.sortBy as MappingSortBy) ?? "logsCount"; -const sortOrder = (params?.sortOrder as SortOrder) ?? "desc"; +const sortBy = validSortBy.includes(params?.sortBy as MappingSortBy) + ? (params?.sortBy as MappingSortBy) + : "logsCount"; +const sortOrder = validSortOrder.includes(params?.sortOrder as SortOrder) + ? (params?.sortOrder as SortOrder) + : "desc";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ee/admin/src/app/model-provider-mappings/page.tsx` around lines 168 - 169, The casts for params?.sortBy and params?.sortOrder are unsafe; add runtime validation before using them by checking params.sortBy against the allowed MappingSortBy values and params.sortOrder against SortOrder (e.g., implement/use isMappingSortBy and isSortOrder helpers or reuse the existing zod schema) and only assign to sortBy and sortOrder when the checks pass, otherwise fall back to the defaults ("logsCount" and "desc"); update the code paths around the symbols sortBy, sortOrder, MappingSortBy, SortOrder and params to use these guards so you no longer rely solely on "as" casts.apps/api/src/routes/admin.ts (2)
2679-2682: Consider pagination performance with large model sets.When
from/toare provided, all matching models are fetched into memory before pagination (merged.slice). This is acceptable if model count is bounded, but could become a concern at scale.For now this is fine given the typical model count, but consider adding a limit on
modelRowsquery if the model catalog grows significantly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/routes/admin.ts` around lines 2679 - 2682, The current code builds `merged` by fetching all matching models then does in-memory pagination with `merged.slice(offset, offset + limit)` which can blow up memory for large catalogs; modify the `modelRows` query to apply a server-side limit and offset (or use a cursor-based query) when `from`/`to` are present so only the requested page is returned, and update the logic that produces `merged`/`paginated` (and the `total` count) to rely on the paginated DB result (or a separate COUNT query) instead of slicing the full `merged` array—look for `modelRows`, `merged`, `paginated`, and the `slice` call and implement DB-side pagination/limit to prevent loading all rows into memory.
767-768: Responserangefield is misleading whenfrom/toare used.When explicit
from/todates are provided, the returnedrangevalue (defaulting to"all") doesn't reflect the actual date range used. Consider returning the actual range used or omitting it when custom dates are provided.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/routes/admin.ts` around lines 767 - 768, The response currently always sets range to query.range ?? "all" in the handler that returns c.json, which is misleading when explicit query.from and/or query.to are provided; update the code (the response construction where range is set) to compute an actualRange: if query.range is present use it, else if query.from or query.to are present format a clear value (e.g., `${query.from ?? ''}-${query.to ?? ''}` or a more readable "from:<date> to:<date>") and set range to that, or omit the range field entirely when custom dates are used—replace the existing range: query.range ?? "all" with this computed actualRange and include that in the JSON response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api/src/routes/admin.ts`:
- Around line 3537-3538: The query params 'limit' and 'offset' currently use
z.coerce.number().optional() without bounds; update the schema for the 'limit'
field to z.coerce.number().min(1).max(1000).optional() and for 'offset' to
z.coerce.number().min(0).max(1000000).optional() (matching the pattern used in
getOrganizations/getModelStats), so negative or unreasonably large values are
rejected by validation.
- Around line 383-401: Validate the incoming from/to date parameters before
using them to set startDate: parse the strings into Date objects and check
validity (e.g., !isNaN(date.getTime())) and that fromDate <= toDate; if invalid,
return a 400 error or fall back to the existing range logic instead of creating
an Invalid Date. Update the logic around startDate/from/to in this handler (the
block that currently sets startDate from from/to and the else branch using
query.range) to perform these checks, and extract a shared helper (e.g.,
validateDateRange or parseAndValidateDates) to reuse in getTimeseries,
getProviderStats, and getModelStats so all endpoints perform the same validation
and error handling.
- Around line 3520-3551: The getModelProviderMappings route is not accepting
date range params so the UI DateRangePicker is ineffective; update the
createRoute for getModelProviderMappings to accept optional from and to query
params (e.g., add z.coerce.date() or z.string() parsers for "from" and "to"
alongside existing search/sort/limit/offset), then apply those params to the
projectHourlyModelStats aggregation query (same date filtering logic used in
getProviderStats) so the stats are restricted to the provided range;
alternatively remove the DateRangePicker from the UI if you intentionally want
all-time stats.
---
Outside diff comments:
In `@ee/admin/src/components/history-chart.tsx`:
- Around line 24-36: The HistoryWindow union includes "30m" but the
windowOptions array omits it, causing a type/UI mismatch; fix by either removing
"30m" from the HistoryWindow type or (preferred since other code references it)
add "30m" back into the windowOptions array so the UI can select it — update the
windowOptions array in history-chart.tsx to include the "30m" entry (matching
label/value format used for other options) and verify related usages (e.g., any
code that consumes HistoryWindow such as date-range-select and admin route
handlers) still accept the value so API and UI remain consistent.
---
Nitpick comments:
In `@apps/api/src/routes/admin.ts`:
- Around line 2679-2682: The current code builds `merged` by fetching all
matching models then does in-memory pagination with `merged.slice(offset, offset
+ limit)` which can blow up memory for large catalogs; modify the `modelRows`
query to apply a server-side limit and offset (or use a cursor-based query) when
`from`/`to` are present so only the requested page is returned, and update the
logic that produces `merged`/`paginated` (and the `total` count) to rely on the
paginated DB result (or a separate COUNT query) instead of slicing the full
`merged` array—look for `modelRows`, `merged`, `paginated`, and the `slice` call
and implement DB-side pagination/limit to prevent loading all rows into memory.
- Around line 767-768: The response currently always sets range to query.range
?? "all" in the handler that returns c.json, which is misleading when explicit
query.from and/or query.to are provided; update the code (the response
construction where range is set) to compute an actualRange: if query.range is
present use it, else if query.from or query.to are present format a clear value
(e.g., `${query.from ?? ''}-${query.to ?? ''}` or a more readable "from:<date>
to:<date>") and set range to that, or omit the range field entirely when custom
dates are used—replace the existing range: query.range ?? "all" with this
computed actualRange and include that in the JSON response.
In `@apps/ui/src/components/date-range-picker.tsx`:
- Around line 340-344: The presets are built once via useMemo(() =>
buildPresets(), []) so they become stale after midnight; update the memo to
depend on a day-derived value (e.g., a currentDate or start-of-day value) so
buildPresets() is re-run when the calendar day changes, and ensure the
activePreset useMemo (which calls findMatchingPreset(from, to, presets)) uses
the same date-derived dependency alongside from/to so matching stays in sync
across midnight.
In `@apps/ui/src/components/date-range-select.tsx`:
- Around line 175-180: The search Input component currently has no placeholder
which can confuse users; update the Input (the JSX element named Input that uses
value={search} and onChange={(e) => setSearch(e.target.value)}) to include a
descriptive placeholder prop (e.g., placeholder="Search…" or "Filter by
name/date") so users understand its purpose, keeping the existing value and
onChange logic intact and ensuring accessibility by choosing concise,
user-friendly text.
In `@ee/admin/src/app/model-provider-mappings/page.tsx`:
- Around line 168-169: The casts for params?.sortBy and params?.sortOrder are
unsafe; add runtime validation before using them by checking params.sortBy
against the allowed MappingSortBy values and params.sortOrder against SortOrder
(e.g., implement/use isMappingSortBy and isSortOrder helpers or reuse the
existing zod schema) and only assign to sortBy and sortOrder when the checks
pass, otherwise fall back to the defaults ("logsCount" and "desc"); update the
code paths around the symbols sortBy, sortOrder, MappingSortBy, SortOrder and
params to use these guards so you no longer rely solely on "as" casts.
In `@ee/admin/src/app/models/page.tsx`:
- Line 70: The dateParams string currently interpolates from and to raw into the
URL (dateParams = from && to ? `&from=${from}&to=${to}` : ""), which can break
if values contain special characters; update the construction of dateParams to
encode both values using encodeURIComponent (e.g., encodeURIComponent(from) and
encodeURIComponent(to)) so it matches how search is handled and ensures safe URL
encoding for the dateParams variable.
In `@ee/admin/src/components/date-range-picker.tsx`:
- Around line 1-468: This file duplicates most of the date-range logic—extract
shared code into the UI package: move MONTH_NAMES, getQuarterLabel,
buildPresets, findMatchingPreset, getDateRangeFromParams (but make default-range
configurable), compareMonth, and MonthRangePicker into apps/ui so both consumers
import them; update DateRangePicker to import those helpers and only keep
plumbing/UI-specific bits (props like buildUrl/path, defaultRangeDays, and
popover align) in the consumer components; ensure MonthRangePicker accepts props
for today/disabled logic if needed and parameterize buildPresets or
getDateRangeFromParams to allow different default ranges (6 vs 29 days) and let
the Popover align be passed in from the caller (align="end" vs "start") so
behavior can be customized without duplicating code.
In `@ee/admin/src/components/model-detail-client.tsx`:
- Around line 17-19: The helper function formatNumber in model-detail-client.tsx
is duplicated in model-provider-charts.tsx; extract it to a shared utility
(e.g., create a new module like lib/format or lib/utils) and export it, then
replace the local definitions in both model-detail-client.tsx and
model-provider-charts.tsx with an import of the shared formatNumber function;
ensure the exported function signature (formatNumber(n: number)): string and its
Intl.NumberFormat("en-US") behavior are preserved and update imports
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a13ec279-d229-49e4-be76-27ff131dd8e0
⛔ Files ignored due to path filters (1)
ee/admin/src/lib/api/v1.d.tsis excluded by!**/v1.d.ts
📒 Files selected for processing (18)
apps/api/src/routes/admin.tsapps/ui/src/components/date-range-picker.tsxapps/ui/src/components/date-range-select.tsxee/admin/src/app/model-provider-mappings/page.tsxee/admin/src/app/models/[modelId]/page.tsxee/admin/src/app/models/page.tsxee/admin/src/app/page.tsxee/admin/src/app/providers/page.tsxee/admin/src/components/admin-shell.tsxee/admin/src/components/date-range-picker.tsxee/admin/src/components/history-chart.tsxee/admin/src/components/model-detail-client.tsxee/admin/src/components/model-provider-charts.tsxee/admin/src/components/models-table.tsxee/admin/src/components/providers-table.tsxee/admin/src/components/time-range-picker.tsxee/admin/src/components/token-time-range-toggle.tsxee/admin/src/lib/types.ts
| if (from && to) { | ||
| startDate = new Date(from + "T00:00:00"); | ||
| startDate.setUTCHours(0, 0, 0, 0); | ||
| } else { | ||
| const range = query.range ?? "all"; | ||
| const rangeDays: Record<string, number | null> = { | ||
| "7d": 7, | ||
| "30d": 30, | ||
| "90d": 90, | ||
| "365d": 365, | ||
| all: null, | ||
| }; | ||
| const days = range in rangeDays ? rangeDays[range] : null; | ||
| if (days !== null) { | ||
| // eslint-disable-next-line no-mixed-operators | ||
| startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); | ||
| startDate.setUTCHours(0, 0, 0, 0); | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing validation for from/to date parameters.
The code parses from and to directly without validating they are valid date strings or that from <= to. Invalid date strings would create Invalid Date objects, causing unexpected query behavior.
🛡️ Proposed fix to add date validation
if (from && to) {
- startDate = new Date(from + "T00:00:00");
+ const parsedFrom = new Date(from + "T00:00:00");
+ const parsedTo = new Date(to + "T00:00:00");
+ if (isNaN(parsedFrom.getTime()) || isNaN(parsedTo.getTime())) {
+ throw new HTTPException(400, { message: "Invalid date format for from/to" });
+ }
+ if (parsedFrom > parsedTo) {
+ throw new HTTPException(400, { message: "from date must be before or equal to to date" });
+ }
+ startDate = parsedFrom;
startDate.setUTCHours(0, 0, 0, 0);This same pattern appears in getTimeseries, getProviderStats, and getModelStats. Consider extracting a shared validation helper.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (from && to) { | |
| startDate = new Date(from + "T00:00:00"); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| } else { | |
| const range = query.range ?? "all"; | |
| const rangeDays: Record<string, number | null> = { | |
| "7d": 7, | |
| "30d": 30, | |
| "90d": 90, | |
| "365d": 365, | |
| all: null, | |
| }; | |
| const days = range in rangeDays ? rangeDays[range] : null; | |
| if (days !== null) { | |
| // eslint-disable-next-line no-mixed-operators | |
| startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| } | |
| } | |
| if (from && to) { | |
| const parsedFrom = new Date(from + "T00:00:00"); | |
| const parsedTo = new Date(to + "T00:00:00"); | |
| if (isNaN(parsedFrom.getTime()) || isNaN(parsedTo.getTime())) { | |
| throw new HTTPException(400, { message: "Invalid date format for from/to" }); | |
| } | |
| if (parsedFrom > parsedTo) { | |
| throw new HTTPException(400, { message: "from date must be before or equal to to date" }); | |
| } | |
| startDate = parsedFrom; | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| } else { | |
| const range = query.range ?? "all"; | |
| const rangeDays: Record<string, number | null> = { | |
| "7d": 7, | |
| "30d": 30, | |
| "90d": 90, | |
| "365d": 365, | |
| all: null, | |
| }; | |
| const days = range in rangeDays ? rangeDays[range] : null; | |
| if (days !== null) { | |
| // eslint-disable-next-line no-mixed-operators | |
| startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/src/routes/admin.ts` around lines 383 - 401, Validate the incoming
from/to date parameters before using them to set startDate: parse the strings
into Date objects and check validity (e.g., !isNaN(date.getTime())) and that
fromDate <= toDate; if invalid, return a 400 error or fall back to the existing
range logic instead of creating an Invalid Date. Update the logic around
startDate/from/to in this handler (the block that currently sets startDate from
from/to and the else branch using query.range) to perform these checks, and
extract a shared helper (e.g., validateDateRange or parseAndValidateDates) to
reuse in getTimeseries, getProviderStats, and getModelStats so all endpoints
perform the same validation and error handling.
| const getModelProviderMappings = createRoute({ | ||
| method: "get", | ||
| path: "/model-provider-mappings", | ||
| request: { | ||
| query: z.object({ | ||
| search: z.string().optional(), | ||
| sortBy: z | ||
| .enum([ | ||
| "modelId", | ||
| "providerId", | ||
| "logsCount", | ||
| "errorsCount", | ||
| "avgTimeToFirstToken", | ||
| "updatedAt", | ||
| ]) | ||
| .optional(), | ||
| sortOrder: z.enum(["asc", "desc"]).optional(), | ||
| limit: z.coerce.number().optional(), | ||
| offset: z.coerce.number().optional(), | ||
| }), | ||
| }, | ||
| responses: { | ||
| 200: { | ||
| content: { | ||
| "application/json": { | ||
| schema: modelProviderMappingsListSchema.openapi({}), | ||
| }, | ||
| }, | ||
| description: "List of all model-provider mappings.", | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
DateRangePicker in UI not wired to API endpoint.
The UI page includes a DateRangePicker component, but the /admin/model-provider-mappings API endpoint doesn't accept from/to query parameters. The stats aggregation queries all-time data from projectHourlyModelStats, making the date picker non-functional for this page.
Either:
- Add
from/toparams to this endpoint and filterprojectHourlyModelStatsby date range (consistent with other admin pages), or - Remove the
DateRangePickerfrom the UI if all-time stats are intentional.
🐛 Proposed fix to add date filtering
const getModelProviderMappings = createRoute({
method: "get",
path: "/model-provider-mappings",
request: {
query: z.object({
search: z.string().optional(),
sortBy: z
.enum([
"modelId",
"providerId",
"logsCount",
"errorsCount",
"avgTimeToFirstToken",
"updatedAt",
])
.optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
limit: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
+ from: z.string().optional(),
+ to: z.string().optional(),
}),
},Then filter projectHourlyModelStats query with date range conditions similar to getProviderStats.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/src/routes/admin.ts` around lines 3520 - 3551, The
getModelProviderMappings route is not accepting date range params so the UI
DateRangePicker is ineffective; update the createRoute for
getModelProviderMappings to accept optional from and to query params (e.g., add
z.coerce.date() or z.string() parsers for "from" and "to" alongside existing
search/sort/limit/offset), then apply those params to the
projectHourlyModelStats aggregation query (same date filtering logic used in
getProviderStats) so the stats are restricted to the provided range;
alternatively remove the DateRangePicker from the UI if you intentionally want
all-time stats.
| limit: z.coerce.number().optional(), | ||
| offset: z.coerce.number().optional(), |
There was a problem hiding this comment.
Missing validation constraints on limit and offset.
Unlike other endpoints in this file (e.g., getOrganizations, getModelStats), the limit and offset params lack .min() and .max() constraints. This could allow negative values or excessively large limits.
🛡️ Proposed fix to add constraints
- limit: z.coerce.number().optional(),
- offset: z.coerce.number().optional(),
+ limit: z.coerce.number().min(1).max(500).default(100).optional(),
+ offset: z.coerce.number().min(0).default(0).optional(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/api/src/routes/admin.ts` around lines 3537 - 3538, The query params
'limit' and 'offset' currently use z.coerce.number().optional() without bounds;
update the schema for the 'limit' field to
z.coerce.number().min(1).max(1000).optional() and for 'offset' to
z.coerce.number().min(0).max(1000000).optional() (matching the pattern used in
getOrganizations/getModelStats), so negative or unreasonably large values are
rejected by validation.
Summary
apps/uiandee/admin: popover with searchable presets (custom, this week/month/year, last 7/30/90 days, quarters, all time) and a two-year-panel month-range calendar for custom ranges?range=with?from=/?to=URL params across all admin pages; extended API endpoints (/admin/providers,/admin/models,/admin/metrics/timeseries) to acceptfrom/toand queryprojectHourlyModelStatsfor date-filtered stats15m,12h,2d,7dto all history charts (removed30m); parent window picker on/models/[id]controls all per-provider charts and the stats cards simultaneously/model-provider-mappingspage: sortable table of all model×provider pairs with status, request counts, error rate, TTFT, and pricing — request counts sourced fromprojectHourlyModelStats(fixes zero counts caused by nullable FK inmodelProviderMappingHistory)Test plan
/models/[id]window picker updates stats cards and all provider charts together/model-provider-mappingsloads, sorts by all columns, search works, request counts are non-zero for active models🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Enhancements