Skip to content

feat(admin): mercury date picker, model mappings page - #1808

Merged
smakosh merged 2 commits into
mainfrom
feat/admin-date-picker-model-mappings
Mar 9, 2026
Merged

smakosh merged 2 commits into
mainfrom
feat/admin-date-picker-model-mappings

Conversation

@smakosh

@smakosh smakosh commented Mar 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Mercury-style date picker in apps/ui and ee/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
  • Admin date params migration: replaced ?range= with ?from=/?to= URL params across all admin pages; extended API endpoints (/admin/providers, /admin/models, /admin/metrics/timeseries) to accept from/to and query projectHourlyModelStats for date-filtered stats
  • Expanded history windows: added 15m, 12h, 2d, 7d to all history charts (removed 30m); parent window picker on /models/[id] controls all per-provider charts and the stats cards simultaneously
  • New /model-provider-mappings page: sortable table of all model×provider pairs with status, request counts, error rate, TTFT, and pricing — request counts sourced from projectHourlyModelStats (fixes zero counts caused by nullable FK in modelProviderMappingHistory)

Test plan

  • Date range picker in apps/ui shows presets, custom calendar works, URL params update
  • Admin dashboard, providers, models pages filter data by selected date range
  • /models/[id] window picker updates stats cards and all provider charts together
  • /model-provider-mappings loads, sorts by all columns, search works, request counts are non-zero for active models

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • Added a new "Model Mappings" admin page displaying model-provider mapping statistics with searchable table, sortable columns, and date range filtering.
  • Introduced an enhanced date range picker with preset options (This week, This month, Last 30 days, etc.) and custom month-based selection.

Enhancements

  • Extended date range filtering across admin metrics, timeseries, providers, and models with explicit date selection.
  • Expanded history time window options to include 15-minute, 12-hour, 2-day, and 7-day intervals.

- 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>
@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3913ddea-d51e-4f81-9271-87e943a9e8cb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces explicit from/to date range parameters to multiple admin API routes (replacing range-based logic), adds a new /model-provider-mappings endpoint with associated schemas, and comprehensively refactors the admin dashboard's date-range picker UI with presets and month-based selection. It includes a new sortable model-provider-mappings admin page with search and date filtering capabilities.

Changes

Cohort / File(s) Summary
API Admin Routes
apps/api/src/routes/admin.ts
Added from and to optional date range parameters to getMetrics, getTimeseries, getProviderStats, and getModelStats routes; refactored logic to use explicit date ranges when provided, falling back to existing range-based calculations. Introduced new getModelProviderMappings endpoint returning model-provider mapping metadata with aggregated stats (logs, errors, cache, TTFT, pricing, context size).
UI Date Range Picker
apps/ui/src/components/date-range-picker.tsx, apps/ui/src/components/date-range-select.tsx
Comprehensive refactor adding preset system with buildPresets, findMatchingPreset, and DatePreset interface; introduced MonthRangePicker component for month-based selection. Replaced dropdown-based date-range-select with Popover-driven UI featuring searchable presets and floating calendar. Exported getDateRangeFromParams utility for public use.
Admin Date Range Picker & Utilities
ee/admin/src/components/date-range-picker.tsx, ee/admin/src/components/time-range-picker.tsx
New comprehensive admin DateRangePicker component with URL-based state synchronization, preset filtering, and month-grid calendar. TimeRangePicker now re-exports DateRangePicker, replacing previous TimeseriesRange-based implementation with from/to string parameters.
Admin Pages - Date Range Migration
ee/admin/src/app/page.tsx, ee/admin/src/app/models/page.tsx, ee/admin/src/app/providers/page.tsx
Replaced TimeRangePicker/range parameter usage with DateRangePicker and from/to string parameters across dashboard, models, and providers pages. Updated API query construction and prop passing to use explicit date ranges instead of named ranges.
New Model-Provider Mappings Page & Components
ee/admin/src/app/model-provider-mappings/page.tsx, ee/admin/src/components/model-detail-client.tsx, ee/admin/src/components/model-provider-charts.tsx
New server-rendered model-provider-mappings admin page with sortable columns, search, and date-range filtering. Introduced ModelDetailClient component for model detail view with per-provider history, KPI metrics, and window selector. Updated ModelProviderCharts to accept external window parameter for controlled history display.
Admin Table & Shell Components
ee/admin/src/components/models-table.tsx, ee/admin/src/components/providers-table.tsx, ee/admin/src/components/admin-shell.tsx
Migrated ModelsTable and ProvidersTable from TimeseriesRange-based range parameter to optional from/to string parameters; updated href-building logic accordingly. Added new "Model Mappings" sidebar navigation item with GitMerge icon.
History & Token Range Components
ee/admin/src/components/history-chart.tsx, ee/admin/src/components/token-time-range-toggle.tsx
Expanded HistoryWindow union to include "15m", "12h", "2d", "7d"; exported windowOptions array. HistoryChart now accepts optional externalWindow prop for controlled window selection. TokenTimeRangeToggle refactored with Popover-based selector replacing inline time-range buttons.
Model Page Detail View
ee/admin/src/app/models/[modelId]/page.tsx
Simplified model detail page by replacing ModelProviderCharts and inline metrics rendering with new ModelDetailClient component; removed local formatting utilities and header KPI sections.
Type Definitions
ee/admin/src/lib/types.ts
Added ModelProviderMappingsResponse and ModelProviderMappingEntry types extending API response types for the new model-provider-mappings domain.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • steebchen
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: introducing a Mercury date picker and a new model mappings page for admin functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/admin-date-picker-model-mappings

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@smakosh smakosh self-assigned this Mar 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Remove "30m" from the HistoryWindow type or add it back to the windowOptions array.

The type definition (line 29) includes "30m", but the windowOptions array (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 in apps/api/src/routes/admin.ts and apps/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 with apps/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, path
Default range 29 days 6 days
URL strategy Uses pathname directly Uses buildUrl()
Popover align end start

Consider 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() captures today at call time, but the useMemo has 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 shared formatNumber helper.

This same function exists in model-provider-charts.tsx (line 14-16). Consider extracting it to a shared utility (e.g., @/lib/utils or @/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 encoding from and to values in URL.

The dateParams string directly interpolates from and to without encodeURIComponent. While date strings (YYYY-MM-DD format) typically don't contain special characters, encoding would be safer for consistency with how search is 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 MappingSortBy and as SortOrder casts 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/to are 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 modelRows query 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: Response range field is misleading when from/to are used.

When explicit from/to dates are provided, the returned range value (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

📥 Commits

Reviewing files that changed from the base of the PR and between b11b062 and 4ed83b4.

⛔ Files ignored due to path filters (1)
  • ee/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (18)
  • apps/api/src/routes/admin.ts
  • apps/ui/src/components/date-range-picker.tsx
  • apps/ui/src/components/date-range-select.tsx
  • ee/admin/src/app/model-provider-mappings/page.tsx
  • ee/admin/src/app/models/[modelId]/page.tsx
  • ee/admin/src/app/models/page.tsx
  • ee/admin/src/app/page.tsx
  • ee/admin/src/app/providers/page.tsx
  • ee/admin/src/components/admin-shell.tsx
  • ee/admin/src/components/date-range-picker.tsx
  • ee/admin/src/components/history-chart.tsx
  • ee/admin/src/components/model-detail-client.tsx
  • ee/admin/src/components/model-provider-charts.tsx
  • ee/admin/src/components/models-table.tsx
  • ee/admin/src/components/providers-table.tsx
  • ee/admin/src/components/time-range-picker.tsx
  • ee/admin/src/components/token-time-range-toggle.tsx
  • ee/admin/src/lib/types.ts

Comment on lines +383 to 401
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +3520 to +3551
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.",
},
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. Add from/to params to this endpoint and filter projectHourlyModelStats by date range (consistent with other admin pages), or
  2. Remove the DateRangePicker from 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.

Comment on lines +3537 to +3538
limit: z.coerce.number().optional(),
offset: z.coerce.number().optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

@smakosh
smakosh added this pull request to the merge queue Mar 9, 2026
Merged via the queue into main with commit 91c7381 Mar 9, 2026
7 checks passed
@smakosh
smakosh deleted the feat/admin-date-picker-model-mappings branch March 9, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants