Skip to content

feat(analytics): sticky range + sortable per-model table columns - #463

Closed
jasnoorgill wants to merge 2 commits into
tashfeenahmed:mainfrom
jasnoorgill:feat/analytics-table-sort
Closed

feat(analytics): sticky range + sortable per-model table columns#463
jasnoorgill wants to merge 2 commits into
tashfeenahmed:mainfrom
jasnoorgill:feat/analytics-table-sort

Conversation

@jasnoorgill

Copy link
Copy Markdown
Contributor

Summary

Two small but related client-side improvements to the analytics page,
shipped together because they share infrastructure and would otherwise
conflict on the same lines of AnalyticsPage.tsx:

  1. Sticky time range — the selected 24h/7d/30d range now persists
    in localStorage so reloads and new sessions land on the user's
    last choice.
  2. Sortable per-model table — every column in the "per-model
    breakdown" table (except pinned, which renders ) is now
    clickable to sort. 3-state cycle: unsorted (API order) → asc → desc → unsorted. Last selection persists in localStorage too.

Both use the same persistence shape as the existing theme toggle
and freellmapi.locale preference. Same useState + useEffect
pattern, same try/catch on every storage access.

Why combined

Originally shipped as PR #462 (sticky range only). Adding the sort
work on top of #462's branch required a rebase that would have
re-written #462's commits — at that point the cleanest move was a
single PR covering both. The two features are thematically adjacent
(analytics-page UX) and small individually, so combining avoids
two PRs reviewing the same file.

Changes

File Change
client/src/pages/AnalyticsPage.tsx +188 / -11. Added useMemo to imports, STORAGE_KEY/loadStoredRange/sort types, SortableHeader helper component, sort state + persistence effect, onHeaderClick reducer, sortedByModel memoized sort, replaced 8 of 9 column headers with <SortableHeader> (pinned stays static), added lucide-react ArrowUp/ArrowDown/ChevronsUpDown imports.

1 file, +188 / -11 across 2 commits.

Sort behavior

State Indicator Order
Unsorted ChevronsUpDown (faded) API-returned (requests DESC)
Asc ArrowUp A→Z / 0→9
Desc ArrowDown Z→A / 9→0
  • Click another column → starts at asc on the new column.
  • Click the same column three times → back to API order, storage
    key removed.
  • Null values sort last in both directions (Excel/Sheets convention).
  • Right-aligned columns flip the indicator with flex-row-reverse so
    the label stays closest to the data.
  • aria-sort attribute is set on each header button for screen readers.

pinned is intentionally not sortable: it renders for zero-pinned
rows, and the 0/>0 distinction is meaningless to sort on.

localStorage keys

Key Value When written
freellmapi.analytics.range '24h' | '7d' | '30d' On range change
freellmapi.analytics.byModelSort {"column": "...", "direction": "asc"|"desc"} or absent On sort change (removed on unsort)

Both keys are validated against an allowlist on read so a corrupted
entry falls back to the default rather than crashing the page. Both
writes are wrapped in try/catch for SSR / private-mode safety.

Test plan

  • npx tsc -b in client/ — clean, 0 errors.
  • npx tsc -b in server/ — 9 errors in server/src/routes/keys.ts
    from upstream's multer typing bug (PR I added Agnes AI, but it doesn’t appear in the model list. #438, pre-existing on
    origin/main at 340f414). Not introduced by this PR
    same errors reproduce on a clean git checkout origin/main.
  • npx vitest run in server/ — 524/524 passing, 0 regressions.
  • Live verification: deployed on integration/all-my-prs @ d08383b.
    Service running on PID 864746, client bundle has both
    freellmapi.analytics.range and freellmapi.analytics.byModelSort
    storage keys + the sort logic.

Manual smoke:

  1. Open /analytics, click 30d. Hard-reload — opens on 30d.
  2. Click any column header (e.g. "Latency") — sorts asc, indicator
    shows ↑, header text darkens.
  3. Click again — sorts desc, indicator shows ↓.
  4. Click a third time — back to API order, indicator faded.
  5. Click a different column — starts at asc on the new column.
  6. Reload — your last sort is restored.
  7. Hard-reload after clicking through to unsort — the key is
    removed from localStorage, the page loads in API order.

Compatibility

No breaking changes. Defaults are unchanged (7d range, API-natural
row order). The persistence is additive. New localStorage keys
are namespaced under freellmapi.* per the existing convention.

Supersedes

Closes #462 — the sticky-range half of this work is included here.

References operator sessions 2026-07-04. No upstream issue number
yet — happy to file one if requested.

Remembers the last-selected 24h/7d/30d range across reloads and new
sessions so the user lands back on the window they were inspecting.
Same localStorage shape as the theme toggle (key: 'theme') and the
locale picker (key: 'freellmapi.locale').

Storage key: 'freellmapi.analytics.range'. Invalid or missing values
fall back to the 7d default; storage errors (quota, private mode) are
swallowed so the page keeps working.
3-state cycle per column: unsorted (API order) → asc → desc → unsorted.
Clicking a different column resets to asc on the new column. Last
selection persists in localStorage so reloads land on the user's
preferred sort (key: 'freellmapi.analytics.byModelSort').

Sortable columns: model, provider, requests, success%, latency,
inTokens, outTokens, saved ($). The 'pinned' column stays static
because it renders '—' for zero-pinned rows and the unsortable
0/>0 distinction would confuse the indicator. Null values sort last
in both directions (Excel/Sheets convention).

Same persistence pattern as the range selector (tashfeenahmed#462) and the
existing 'theme' / 'freellmapi.locale' keys. Right-aligned columns
flip the sort-indicator with flex-row-reverse so the label stays
closest to the data. aria-sort attributes set on the buttons for
screen readers.
@tashfeenahmed

Copy link
Copy Markdown
Owner

Thanks, the sticky range and sortable columns work looks good and CI was green. Main has since shipped the redesigned Analytics page (#467) plus a new Recent Calls table (#474), so this now conflicts on AnalyticsPage.tsx. Could you rebase onto latest main and re-apply the sticky range and sort on the new table layout? Happy to merge once it's green again.

@jasnoorgill

jasnoorgill commented Jul 7, 2026 via email

Copy link
Copy Markdown
Contributor Author

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